summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorjmaslek <jmaslek11@gmail.com>2021-09-16 12:20:53 -0400
committerGitHub <noreply@github.com>2021-09-16 12:20:53 -0400
commit5a809d262d043663340d7a64d2aa5fd5dcc38f05 (patch)
tree1fd7f4f75ae58bb0ff0e18f195d04aeb8094d6ba
parent20eaaab5ac89ffe060db8934db4ae005cc5834f0 (diff)
Refactor port-> po and pa menus (#748)
* Refactor property weightings * Refactor pypfopt commands * refactor pa menu * Address review comments * Typo
-rw-r--r--gamestonk_terminal/helper_funcs.py33
-rw-r--r--gamestonk_terminal/portfolio/portfolio_analysis/pa_controller.py206
-rw-r--r--gamestonk_terminal/portfolio/portfolio_analysis/portfolio_model.py62
-rw-r--r--gamestonk_terminal/portfolio/portfolio_analysis/portfolio_parser.py179
-rw-r--r--gamestonk_terminal/portfolio/portfolio_analysis/portfolio_view.py32
-rw-r--r--gamestonk_terminal/portfolio/portfolio_analysis/portfolios/my_portfolio.csv1
-rw-r--r--gamestonk_terminal/portfolio/portfolio_controller.py10
-rw-r--r--gamestonk_terminal/portfolio/portfolio_optimization/optimizer_helper.py179
-rw-r--r--gamestonk_terminal/portfolio/portfolio_optimization/optimizer_model.py254
-rw-r--r--gamestonk_terminal/portfolio/portfolio_optimization/optimizer_view.py1001
-rw-r--r--gamestonk_terminal/portfolio/portfolio_optimization/po_controller.py663
-rw-r--r--gamestonk_terminal/portfolio/portfolio_optimization/yahoo_finance_model.py32
-rw-r--r--tests/test_portfolio_optimization/test_optimizer_view.py93
13 files changed, 1557 insertions, 1188 deletions
diff --git a/gamestonk_terminal/helper_funcs.py b/gamestonk_terminal/helper_funcs.py
index dd8196acdab..5e800a04fdf 100644
--- a/gamestonk_terminal/helper_funcs.py
+++ b/gamestonk_terminal/helper_funcs.py
@@ -27,39 +27,6 @@ if cfgPlot.BACKEND is not None:
matplotlib.use(cfgPlot.BACKEND)
-def check_valid_path(path: str) -> str:
- """Argparse type function to test is path is valid
-
- Parameters
- ----------
- path: str
- Path supplied
-
- Returns
- -------
- path: str
- Valid path
-
- Raises
- -------
- argparse.ArgumentTypeError
- Given path does not exist
- """
- if not os.path.exists(
- os.path.abspath(
- os.path.join(
- "gamestonk_terminal",
- "portfolio",
- "portfolio_analysis",
- "portfolios",
- f"{path}.csv",
- )
- )
- ):
- raise argparse.ArgumentTypeError("Path does not exist")
- return path
-
-
def check_int_range(mini: int, maxi: int):
"""Checks if argparse argument is an int between 2 values.
diff --git a/gamestonk_terminal/portfolio/portfolio_analysis/pa_controller.py b/gamestonk_terminal/portfolio/portfolio_analysis/pa_controller.py
index 53f2027fb3a..bba2a3e222f 100644
--- a/gamestonk_terminal/portfolio/portfolio_analysis/pa_controller.py
+++ b/gamestonk_terminal/portfolio/portfolio_analysis/pa_controller.py
@@ -1,15 +1,28 @@
+"""Portffolio Analysis Controller"""
__docformat__ = "numpy"
import argparse
import os
+from pathlib import Path
+
import pandas as pd
from prompt_toolkit.completion import NestedCompleter
from gamestonk_terminal import feature_flags as gtff
-from gamestonk_terminal.helper_funcs import get_flair
+from gamestonk_terminal.helper_funcs import (
+ get_flair,
+ parse_known_args_and_warn,
+)
from gamestonk_terminal.menu import session
+from gamestonk_terminal.portfolio.portfolio_analysis import (
+ portfolio_model,
+ portfolio_view,
+)
-from gamestonk_terminal.portfolio.portfolio_analysis import portfolio_parser
+portfolios_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "portfolios")
+possible_paths = [
+ Path(port).stem for port in os.listdir(portfolios_path) if port.endswith(".csv")
+]
class PortfolioController:
@@ -21,10 +34,15 @@ class PortfolioController:
"help",
"q",
"quit",
+ ]
+ CHOICES_COMMANDS = [
+ "view",
"load",
"group",
]
+ CHOICES += CHOICES_COMMANDS
+
def __init__(self):
self.pa_parser = argparse.ArgumentParser(add_help=False, prog="pa")
self.pa_parser.add_argument("cmd", choices=self.CHOICES)
@@ -33,22 +51,24 @@ class PortfolioController:
def print_help(self):
"""Print help"""
- print(
- "https://github.com/GamestonkTerminal/GamestonkTerminal/tree/main/gamestonk_terminal/portfolio_analysis"
- )
- print("\nPortfolio Analysis:")
- print(" cls clear screen")
- print(" ?/help show this menu again")
- print(" q quit this menu, and shows back to main menu")
- print(" quit quit to abandon program")
- print("")
- print(" load load portfolio from csv file")
- print("")
- if self.portfolio_name:
- print(f"Portfolio: {self.portfolio_name}")
- print("")
- print(" group view holdings by a user input group")
- print("")
+ help_string = f"""https://github.com/GamestonkTerminal/GamestonkTerminal/tree/main/gamestonk_terminal/portfolio_analysis
+
+>>PORTFOLIO ANALYSIS<<
+
+What would you like to do?
+ cls clear screen
+ ?/help show this menu again
+ q quit this menu, and shows back to main menu
+ quit quit to abandon program
+
+ view view available portfolios
+ load load portfolio from csv file
+
+Portfolio: {self.portfolio_name or None}
+
+ group view holdings grouped by parameter
+ """
+ print(help_string)
def switch(self, an_input: str):
"""Process and dispatch input
@@ -94,21 +114,153 @@ class PortfolioController:
"""Process Quit command - quit the program"""
return True
+ # TODO: allow loading other files than csv
def call_load(self, other_args):
- """Process csv command"""
- self.portfolio_name, self.portfolio = portfolio_parser.load_csv_portfolio(
- other_args
+ """Process load command"""
+ parser = argparse.ArgumentParser(
+ prog="load",
+ add_help=False,
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ description="Function to get portfolio from predefined csv file inside portfolios folder",
+ )
+ parser.add_argument(
+ "-s",
+ "--sector",
+ action="store_true",
+ default=False,
+ help="Add sector to dataframe",
+ dest="sector",
+ )
+ parser.add_argument(
+ "--no_last_price",
+ action="store_false",
+ default=True,
+ help="Don't 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="my_portfolio",
+ choices=possible_paths,
+ help="Path to csv file",
+ dest="path",
)
- if self.portfolio_name:
- print(f"Successfully loaded: {self.portfolio_name}\n")
+ try:
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return
+
+ self.portfolio_name = ns_parser.path
+ self.portfolio = portfolio_model.load_csv_portfolio(
+ full_path=os.path.join(portfolios_path, ns_parser.path) + ".csv",
+ sector=ns_parser.sector,
+ last_price=ns_parser.last_price,
+ show_nan=ns_parser.show_nan,
+ )
+ if not self.portfolio.empty:
+ print(f"Successfully loaded: {self.portfolio_name}\n")
+
+ except Exception as e:
+ print(e)
def call_group(self, other_args):
"""Process group command"""
- if self.portfolio_name:
- portfolio_parser.breakdown_by_group(self.portfolio, other_args)
- else:
- print("Please load a portfolio")
+ parser = argparse.ArgumentParser(
+ prog="group",
+ add_help=False,
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ description="Displays portfolio grouped by a given column",
+ )
+ if other_args and "-" not in other_args[0]:
+ other_args.insert(0, "-g")
+ parser.add_argument(
+ "-g",
+ "--group",
+ type=str,
+ dest="group",
+ default="Ticker",
+ choices=self.portfolio.columns,
+ 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
+
+ if "value" not in self.portfolio.columns:
+ print(
+ "'value' column not in portfolio. Either add manually or load without --no_last_price flag\n"
+ )
+ return
+
+ portfolio_view.display_group_holdings(
+ portfolio=self.portfolio, group_column=ns_parser.group
+ )
+
+ except Exception as e:
+ print(e, "\n")
+
+ def call_view(self, other_args):
+ parser = argparse.ArgumentParser(
+ prog="view",
+ add_help=False,
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ description="Show available portfolios to load.",
+ )
+ parser.add_argument(
+ "-f",
+ "-format",
+ choices=["csv", "all"],
+ help="Format of portfolios to view. 'csv' will show all csv files available, etc.",
+ default="all",
+ dest="file_format",
+ )
+
+ try:
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return
+
+ available_ports = os.listdir(portfolios_path)
+ if ns_parser.file_format != "all":
+ available_ports = [
+ port
+ for port in available_ports
+ if port.endswith(ns_parser.file_format)
+ ]
+
+ print("\nAvailable Portfolios:\n")
+ for port in available_ports:
+ print(Path(port).stem)
+ print("")
+
+ except Exception as e:
+ print(e, "\n")
def menu():
diff --git a/gamestonk_terminal/portfolio/portfolio_analysis/portfolio_model.py b/gamestonk_terminal/portfolio/portfolio_analysis/portfolio_model.py
new file mode 100644
index 00000000000..3301ac697e2
--- /dev/null
+++ b/gamestonk_terminal/portfolio/portfolio_analysis/portfolio_model.py
@@ -0,0 +1,62 @@
+"""Portfolio Model"""
+__docformat__ = "numpy"
+
+from tabulate import tabulate
+import pandas as pd
+import yfinance as yf
+import gamestonk_terminal.feature_flags as gtff
+
+# pylint: disable=no-member,unsupported-assignment-operation,unsubscriptable-object
+
+
+def load_csv_portfolio(
+ full_path: str,
+ sector: bool = False,
+ last_price: bool = False,
+ show_nan: bool = True,
+) -> pd.DataFrame:
+ """Loads a csv portfolio into a dataframe and adds sector and last price
+
+ Parameters
+ ----------
+ full_path : str
+ Path to csv portfolio.
+ sector : bool, optional
+ Boolean to indicate getting sector from yfinance , by default False
+ last_price : bool, optional
+ Boolean to indicate getting last price from yfinance, by default False
+ show_nan : bool, optional
+ Boolean to indicate dropping nan values, by default True
+
+ Returns
+ -------
+ pd.DataFrame
+ Dataframe conataining csv portfolio
+ """
+ df = pd.read_csv(full_path)
+
+ if 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 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 show_nan:
+ df = df.dropna(axis=1)
+
+ if gtff.USE_TABULATE_DF:
+ print(tabulate(df, tablefmt="fancy_grid", headers=df.columns), "\n")
+ else:
+ print(df.to_string(), "\n")
+ return df
diff --git a/gamestonk_terminal/portfolio/portfolio_analysis/portfolio_parser.py b/gamestonk_terminal/portfolio/portfolio_analysis/portfolio_parser.py
deleted file mode 100644
index 051bb2163b5..00000000000
--- a/gamestonk_terminal/portfolio/portfolio_analysis/portfolio_parser.py
+++ /dev/null
@@ -1,179 +0,0 @@
-"""Portfolio parser module"""
-__docformat__ = "numpy"
-
-import os
-import argparse
-from typing import List, Tuple
-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
-
-# pylint: disable=no-member,unsupported-assignment-operation,unsubscriptable-object
-
-
-def load_csv_portfolio(other_args: List[str]) -> Tuple[str, pd.DataFrame]:
- """Load portfolio from csv
-
- Parameters
- ----------
- other_args: List[str]
- Argparse arguments
-
- Returns
- ----------
- portfolio_name : str
- Portfolio name
- portfolio : pd.DataFrame
- Portfolio dataframe
- """
- parser = argparse.ArgumentParser(
- prog="load",
- add_help=False,
- formatter_class=argparse.ArgumentDefaultsHelpFormatter,
- description="Function to get portfolio from predefined csv file inside portfolios folder",
- )
- parser.add_argument(
- "-p",
- "--path",
- default="my_portfolio",
- type=check_valid_path,
- help="Path to csv file",
- dest="path",
- )
- 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",
- )
-
- try:
- ns_parser = parse_known_args_and_warn(parser, other_args)
- if not ns_parser:
- return "", pd.DataFrame()
-
- full_path = os.path.abspath(
- os.path.join(
- "gamestonk_terminal",
- "portfolio",
- "portfolio_analysis",
- "portfolios",
- f"{ns_parser.path}.csv",
- )
- )
- df = pd.read_csv(full_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 ns_parser.path, 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,
- formatter_class=argparse.ArgumentDefaultsHelpFormatter,
- 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/portfolio_analysis/portfolio_view.py b/gamestonk_terminal/portfolio/portfolio_analysis/portfolio_view.py
new file mode 100644
index 00000000000..b03131ea12c
--- /dev/null
+++ b/gamestonk_terminal/portfolio/portfolio_analysis/portfolio_view.py
@@ -0,0 +1,32 @@
+"""Portfolio View"""
+__docformat__ = "numpy"
+
+import pandas as pd
+from tabulate import tabulate
+import gamestonk_terminal.feature_flags as gtff
+
+
+def display_group_holdings(portfolio: pd.DataFrame, group_column: str):
+ """Display portfolio holdings based on grouping
+
+ Parameters
+ ----------
+ portfolio : pd.DataFrame
+ Portfolio dataframe
+ group_column : str
+ Column to group by
+ """
+
+ grouped_df = pd.DataFrame(portfolio.groupby(group_column).agg(sum)["value"])
+ if gtff.USE_TABULATE_DF:
+ print(
+ tabulate(
+ grouped_df,
+ headers=[group_column, "value"],
+ tablefmt="fancy_grid",
+ floatfmt=".2f",
+ ),
+ "\n",
+ )
+ else:
+ print(portfolio.to_string(), "\n")
diff --git a/gamestonk_terminal/portfolio/portfolio_analysis/portfolios/my_portfolio.csv b/gamestonk_terminal/portfolio/portfolio_analysis/portfolios/my_portfolio.csv
index 9123480e064..99482ebce9d 100644
--- a/gamestonk_terminal/portfolio/portfolio_analysis/portfolios/my_portfolio.csv
+++ b/gamestonk_terminal/portfolio/portfolio_analysis/portfolios/my_portfolio.csv
@@ -4,3 +4,4 @@ AMC,420,
MSFT,3,
BAC,2,
O,1,
+AAPL,4
diff --git a/gamestonk_terminal/portfolio/portfolio_controller.py b/gamestonk_terminal/portfolio/portfolio_controller.py
index 2e4116c36d2..8870e39f110 100644
--- a/gamestonk_terminal/portfolio/portfolio_controller.py
+++ b/gamestonk_terminal/portfolio/portfolio_controller.py
@@ -1,3 +1,6 @@
+"""Portfolio Controller"""
+__docformat__ = "numpy"
+
import argparse
import os
@@ -25,13 +28,6 @@ class PortfolioController:
]
CHOICES_MENUS = [
- "load",
- "quote",
- "candle",
- "view",
- ]
-
- CHOICES_MENUS = [
"bro",
"pa",
"po",
diff --git a/gamestonk_terminal/portfolio/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio/portfolio_optimization/optimizer_helper.py
index cbd20d60732..a9866b09afd 100644
--- a/gamestonk_terminal/portfolio/portfolio_optimization/optimizer_helper.py
+++ b/gamestonk_terminal/portfolio/portfolio_optimization/optimizer_helper.py
@@ -1,20 +1,9 @@
-""" Portfolio Optimization Helper Functions """
+"""Optimization helpers"""
__docformat__ = "numpy"
import argparse
-from typing import List
-import math
-import pandas as pd
-import numpy as np
-import matplotlib.pyplot as plt
-import yfinance as yf
-from pypfopt.efficient_frontier import EfficientFrontier
-from pypfopt import risk_models
-from pypfopt import expected_returns
-from gamestonk_terminal.config_plot import PLOT_DPI
-from gamestonk_terminal import feature_flags as gtff
-from gamestonk_terminal.helper_funcs import plot_autoscale
+# These are all the possible yfinance properties
l_valid_property_infos = [
"previousClose",
"regularMarketOpen",
@@ -116,167 +105,3 @@ def check_valid_property_type(aproperty: str) -> str:
return aproperty
raise argparse.ArgumentTypeError(f"{aproperty} is not a valid info")
-
-
-def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFrame:
- """Get adjusted closing price for each stock in the list
-
- Parameters
- ----------
- list_of_stocks: List[str]
- List of tickers to get historical data for
- period: str
- Period to get data from yfinance
-
- Returns
- -------
- stock_closes: DataFrame
- DataFrame containing daily (adjusted) close prices for each stock in list
- """
-
- stock_prices = yf.download(
- list_of_stocks, period=period, progress=False, group_by="ticker"
- )
- stock_closes = pd.DataFrame(index=stock_prices.index)
- for stock in list_of_stocks:
- stock_closes[stock] = stock_prices[stock]["Adj Close"]
- return stock_closes
-
-
-def prepare_efficient_frontier(stock_prices: pd.DataFrame):
- """Take in a dataframe of prices and return an efficient frontier object
-
- Parameters
- ----------
- stock_prices : DataFrame
- DataFrame where indices are DateTime and columns are stocks
-
- Returns
- -------
- ef: EfficientFrontier
- EfficientFrontier object
- """
-
- mu = expected_returns.mean_historical_return(stock_prices)
- S = risk_models.sample_cov(stock_prices)
- ef = EfficientFrontier(mu, S)
- return ef
-
-
-def display_weights(weights: dict):
- """Print weights in a nice format
-
- Parameters
- ----------
- weights: dict
- weights to display. Keys are stocks. Values are either weights or values if -v specified
- """
-
- if not weights:
- return
- weight_df = pd.DataFrame.from_dict(data=weights, orient="index", columns=["value"])
- if math.isclose(weight_df.sum()["value"], 1, rel_tol=0.1):
- weight_df["weight"] = (weight_df["value"] * 100).astype(str).apply(
- lambda s: " " + s[:4] if s.find(".") == 1 else "" + s[:5]
- ) + " %"
- print(pd.DataFrame(weight_df["weight"]).to_string(header=False))
- else:
- print(weight_df.to_string(header=False))
-
-
-def my_autopct(x):
- """Function for autopct of plt.pie. This results in values not being printed in the pie if they are 'too small'"""
- if x > 4:
- return f"{x:.2f} %"
-
- return ""
-
-
-def pie_chart_weights(weights: dict, title_opt: str):
- """Show a pie chart of holdings
-
- Parameters
- ----------
- weights: dict
- Weights to display, where keys are tickers, and values are either weights or values if -v specified
- title: str
- Title to be used on the plot title
- """
- if not weights:
- return
-
- init_stocks = list(weights.keys())
- init_sizes = list(weights.values())
- stocks = []
- sizes = []
- for stock, size in zip(init_stocks, init_sizes):
- if size > 0:
- stocks.append(stock)
- sizes.append(size