From b880f34053d399fffa51ac703050664df32b3c24 Mon Sep 17 00:00:00 2001 From: northern-64bit <75195383+northern-64bit@users.noreply.github.com> Date: Sat, 14 Jan 2023 13:47:52 +0100 Subject: [PATCH 01/12] Fix issue 3960 --- openbb_terminal/portfolio/portfolio_model.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/portfolio/portfolio_model.py b/openbb_terminal/portfolio/portfolio_model.py index be7429a1fe1..c61a814a659 100644 --- a/openbb_terminal/portfolio/portfolio_model.py +++ b/openbb_terminal/portfolio/portfolio_model.py @@ -727,6 +727,13 @@ def get_summary( portfolio_engine.benchmark_returns, window ) + r2_portfolio_returns = portfolio_returns.copy() + r2_benchmark_returns = benchmark_returns.copy() + if len(portfolio_returns) > len(benchmark_returns): + r2_portfolio_returns = r2_portfolio_returns[r2_portfolio_returns.index.isin(r2_benchmark_returns.index)] + elif len(portfolio_returns) < len(benchmark_returns): + r2_benchmark_returns = r2_benchmark_returns[r2_benchmark_returns.index.isin(r2_portfolio_returns.index)] + metrics = { "Volatility": [portfolio_returns.std(), benchmark_returns.std()], "Skew": [ @@ -750,8 +757,8 @@ def get_summary( metrics_model.sortino_ratio(benchmark_returns, risk_free_rate), ], "R2 Score": [ - r2_score(portfolio_returns, benchmark_returns), - r2_score(portfolio_returns, benchmark_returns), + r2_score(r2_portfolio_returns, r2_benchmark_returns), + r2_score(r2_portfolio_returns, r2_benchmark_returns), ], } From 71728ec2640bf9f749558267e38f5accb031691b Mon Sep 17 00:00:00 2001 From: northern-64bit <75195383+northern-64bit@users.noreply.github.com> Date: Sat, 14 Jan 2023 13:59:36 +0100 Subject: [PATCH 02/12] Linting --- openbb_terminal/portfolio/portfolio_model.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/portfolio/portfolio_model.py b/openbb_terminal/portfolio/portfolio_model.py index c61a814a659..8d228d5c3e9 100644 --- a/openbb_terminal/portfolio/portfolio_model.py +++ b/openbb_terminal/portfolio/portfolio_model.py @@ -730,9 +730,13 @@ def get_summary( r2_portfolio_returns = portfolio_returns.copy() r2_benchmark_returns = benchmark_returns.copy() if len(portfolio_returns) > len(benchmark_returns): - r2_portfolio_returns = r2_portfolio_returns[r2_portfolio_returns.index.isin(r2_benchmark_returns.index)] + r2_portfolio_returns = r2_portfolio_returns[ + r2_portfolio_returns.index.isin(r2_benchmark_returns.index) + ] elif len(portfolio_returns) < len(benchmark_returns): - r2_benchmark_returns = r2_benchmark_returns[r2_benchmark_returns.index.isin(r2_portfolio_returns.index)] + r2_benchmark_returns = r2_benchmark_returns[ + r2_benchmark_returns.index.isin(r2_portfolio_returns.index) + ] metrics = { "Volatility": [portfolio_returns.std(), benchmark_returns.std()], From 469f11d39a8005147ebedb95891f1c8dad914a90 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Sun, 5 Feb 2023 12:07:28 +0000 Subject: [PATCH 03/12] leave false --- openbb_terminal/portfolio/portfolio_engine.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openbb_terminal/portfolio/portfolio_engine.py b/openbb_terminal/portfolio/portfolio_engine.py index d683becef08..b52f639a5cc 100644 --- a/openbb_terminal/portfolio/portfolio_engine.py +++ b/openbb_terminal/portfolio/portfolio_engine.py @@ -227,7 +227,7 @@ class PortfolioEngine: 13. Populate fields Sector, Industry and Country """ - p_bar = tqdm(range(14), desc="Preprocessing transactions") + p_bar = tqdm(range(14), desc="Preprocessing transactions", leave=False) try: # 0. If optional fields not in the transactions add missing @@ -513,7 +513,7 @@ class PortfolioEngine: True if successful, False otherwise """ - p_bar = tqdm(range(4), desc=" Loading benchmark") + p_bar = tqdm(range(4), desc=" Loading benchmark", leave=False) self.benchmark_historical_prices = yf.download( symbol, @@ -672,7 +672,9 @@ class PortfolioEngine: whether to use close or adjusted close prices """ - p_bar = tqdm(range(len(self.tickers)), desc=" Loading price data") + p_bar = tqdm( + range(len(self.tickers)), desc=" Loading price data", leave=False + ) for ticker_type, data in self.tickers.items(): price_data = yf.download( @@ -803,7 +805,7 @@ class PortfolioEngine: from any sales during the period [Cash Inflow(t)]. """ - p_bar = tqdm(range(1), desc=" Calculating returns") + p_bar = tqdm(range(1), desc=" Calculating returns", leave=False) trade_data = self.historical_trade_data From 2ccc503ddf2ea7f21d32b0ce75883f6a908c0634 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Sun, 5 Feb 2023 12:08:13 +0000 Subject: [PATCH 04/12] black --- openbb_terminal/portfolio/portfolio_controller.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openbb_terminal/portfolio/portfolio_controller.py b/openbb_terminal/portfolio/portfolio_controller.py index 14a140d8e7b..bfd5be83dcb 100644 --- a/openbb_terminal/portfolio/portfolio_controller.py +++ b/openbb_terminal/portfolio/portfolio_controller.py @@ -440,7 +440,9 @@ class PortfolioController(BaseController): "[red]Please first load transactions file using 'load'[/red]" ) else: - if self.portfolio.set_benchmark(ns_parser.benchmark, ns_parser.full_shares): + if self.portfolio.set_benchmark( + ns_parser.benchmark, ns_parser.full_shares + ): self.benchmark_name = statics.BENCHMARK_CHOICES.get( ns_parser.benchmark, ns_parser.benchmark ) From db693be042ad1066aeb8a216c4027f9ead71063f Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Sun, 5 Feb 2023 13:11:47 +0000 Subject: [PATCH 05/12] handle some error cases --- .../portfolio/test_portfolio.openbb | 2 + openbb_terminal/portfolio/allocation_model.py | 73 +++++++++++-------- .../portfolio/portfolio_controller.py | 30 +++++--- openbb_terminal/portfolio/portfolio_engine.py | 5 +- openbb_terminal/portfolio/portfolio_view.py | 20 ++++- 5 files changed, 83 insertions(+), 47 deletions(-) diff --git a/openbb_terminal/miscellaneous/integration_tests_scripts/portfolio/test_portfolio.openbb b/openbb_terminal/miscellaneous/integration_tests_scripts/portfolio/test_portfolio.openbb index 231ff561866..572a3955c5b 100644 --- a/openbb_terminal/miscellaneous/integration_tests_scripts/portfolio/test_portfolio.openbb +++ b/openbb_terminal/miscellaneous/integration_tests_scripts/portfolio/test_portfolio.openbb @@ -2,6 +2,8 @@ portfolio load --example show bench QQQ +bench FAKE_BENCH +bench TSLA alloc assets -t -l 4 alloc sectors -t -l 4 alloc countries -t -l 4 diff --git a/openbb_terminal/portfolio/allocation_model.py b/openbb_terminal/portfolio/allocation_model.py index 9cdf7b933cf..606f06fdef3 100644 --- a/openbb_terminal/portfolio/allocation_model.py +++ b/openbb_terminal/portfolio/allocation_model.py @@ -72,11 +72,16 @@ def get_assets_allocation( pd.DataFrame DataFrame with the portfolio's asset allocations """ - benchmark_assets_allocation = pd.DataFrame(benchmark_info["holdings"]) - benchmark_assets_allocation.rename( - columns={"symbol": "Symbol", "holdingPercent": "Benchmark"}, inplace=True - ) - benchmark_assets_allocation.drop(columns=["holdingName"], inplace=True) + if "holdings" in benchmark_info: + benchmark_assets_allocation = pd.DataFrame(benchmark_info["holdings"]) + benchmark_assets_allocation.rename( + columns={"symbol": "Symbol", "holdingPercent": "Benchmark"}, inplace=True + ) + benchmark_assets_allocation.drop(columns=["holdingName"], inplace=True) + else: + benchmark_assets_allocation = pd.DataFrame( + columns=["Symbol", "Benchmark", "Sector", "Country"] + ) portfolio_assets_allocation = ( portfolio_trades[portfolio_trades["Type"] != "CASH"] @@ -109,34 +114,37 @@ def get_sectors_allocation( Returns ------- pd.DataFrame - DataFrame with regional allocations. + DataFrame with benchmark allocations. pd.DataFrame - DataFrame with country allocations + DataFrame with portfolio allocations """ - benchmark_sectors_allocation = ( - pd.DataFrame.from_dict( - data={ - sector_name: allocation - for sector in benchmark_info["sectorWeightings"] - for sector_name, allocation in sector.items() - }, - orient="index", + if "sectorWeightings" in benchmark_info: + benchmark_sectors_allocation = ( + pd.DataFrame.from_dict( + data={ + sector_name: allocation + for sector in benchmark_info["sectorWeightings"] + for sector_name, allocation in sector.items() + }, + orient="index", + ) + .squeeze() + .sort_values(ascending=False) ) - .squeeze() - .sort_values(ascending=False) - ) - # Prettify sector allocations of benchmark to align with Portfolio Excel - prettified = [ - sector.replace("_", " ").title() - for sector in benchmark_sectors_allocation.index - ] + # Prettify sector allocations of benchmark to align with Portfolio Excel + prettified = [ + sector.replace("_", " ").title() + for sector in benchmark_sectors_allocation.index + ] - benchmark_sectors_allocation.index = prettified - benchmark_sectors_allocation = pd.DataFrame(benchmark_sectors_allocation) - benchmark_sectors_allocation.reset_index(inplace=True) - benchmark_sectors_allocation.columns = ["Sector", "Benchmark"] + benchmark_sectors_allocation.index = prettified + benchmark_sectors_allocation = pd.DataFrame(benchmark_sectors_allocation) + benchmark_sectors_allocation.reset_index(inplace=True) + benchmark_sectors_allocation.columns = ["Sector", "Benchmark"] + else: + benchmark_sectors_allocation = pd.DataFrame(columns=["Sector", "Benchmark"]) # Define portfolio sector allocation # Aggregate sector value for stocks and crypto @@ -243,9 +251,9 @@ def get_countries_allocation( Returns ------- pd.DataFrame - DataFrame with regional allocations. + DataFrame with benchmark allocations. pd.DataFrame - DataFrame with country allocations + DataFrame with portfolio allocations """ benchmark_allocation = get_symbol_allocation( @@ -305,7 +313,7 @@ def get_symbol_allocation( Returns ------- pd.DataFrame - Dictionary with country allocations + Dictionary with category allocations """ if category == "Region": @@ -332,7 +340,10 @@ def get_symbol_allocation( # Collect data from Fidelity about the portfolio composition of the benchmark URL = f"https://screener.fidelity.com/ftgw/etf/goto/snapshot/portfolioComposition.jhtml?symbols={symbol}" html = request(URL).content - df_list = pd.read_html(html) + try: + df_list = pd.read_html(html) + except ValueError: + return pd.DataFrame(columns=[category, col_name]) # Find the ones that contain regions and countries for index, item in enumerate(df_list): diff --git a/openbb_terminal/portfolio/portfolio_controller.py b/openbb_terminal/portfolio/portfolio_controller.py index bfd5be83dcb..3551fe12b57 100644 --- a/openbb_terminal/portfolio/portfolio_controller.py +++ b/openbb_terminal/portfolio/portfolio_controller.py @@ -141,6 +141,7 @@ class PortfolioController(BaseController): self.portfolio_name: str = "" self.benchmark_name: str = "" self.original_benchmark_ticker = "" + self.recalculate_alloc = False self.risk_free_rate = 0 self.portlist: List[str] = os.listdir(self.DEFAULT_HOLDINGS_PATH) self.portfolio = None @@ -447,6 +448,7 @@ class PortfolioController(BaseController): ns_parser.benchmark, ns_parser.benchmark ) self.original_benchmark_ticker = ns_parser.benchmark + self.recalculate_alloc = True @log_start_end(log=logger) def call_alloc(self, other_args: List[str]): @@ -488,27 +490,31 @@ class PortfolioController(BaseController): ): if ns_parser.agg == "assets": portfolio_view.display_assets_allocation( - self.portfolio, - ns_parser.limit, - ns_parser.tables, + portfolio_engine=self.portfolio, + limit=ns_parser.limit, + tables=ns_parser.tables, + recalculate=self.recalculate_alloc, ) elif ns_parser.agg == "sectors": portfolio_view.display_sectors_allocation( - self.portfolio, - ns_parser.limit, - ns_parser.tables, + portfolio_engine=self.portfolio, + limit=ns_parser.limit, + tables=ns_parser.tables, + recalculate=self.recalculate_alloc, ) elif ns_parser.agg == "countries": portfolio_view.display_countries_allocation( - self.portfolio, - ns_parser.limit, - ns_parser.tables, + portfolio_engine=self.portfolio, + limit=ns_parser.limit, + tables=ns_parser.tables, + recalculate=self.recalculate_alloc, ) elif ns_parser.agg == "regions": portfolio_view.display_regions_allocation( - self.portfolio, - ns_parser.limit, - ns_parser.tables, + portfolio_engine=self.portfolio, + limit=ns_parser.limit, + tables=ns_parser.tables, + recalculate=self.recalculate_alloc, ) else: console.print( diff --git a/openbb_terminal/portfolio/portfolio_engine.py b/openbb_terminal/portfolio/portfolio_engine.py index b52f639a5cc..56d3b26410e 100644 --- a/openbb_terminal/portfolio/portfolio_engine.py +++ b/openbb_terminal/portfolio/portfolio_engine.py @@ -118,7 +118,7 @@ class PortfolioEngine: self.tickers_list = None self.tickers: Dict[Any, Any] = {} self.benchmark_ticker: str = "" - self.benchmark_info = None + self.benchmark_info: Dict[Any, Any] = {} self.historical_trade_data = pd.DataFrame() # Portfolio @@ -560,7 +560,8 @@ class PortfolioEngine: p_bar.refresh() try: - self.benchmark_info = yf.Ticker(symbol).info + self.benchmark_info = dict(yf.Ticker(symbol).info) + self.benchmark_info["symbol"] = symbol except Exception as _: # noqa console.print( f"[red]\n\nCould not get info for {symbol}." diff --git a/openbb_terminal/portfolio/portfolio_view.py b/openbb_terminal/portfolio/portfolio_view.py index e5734881216..132ca92a484 100644 --- a/openbb_terminal/portfolio/portfolio_view.py +++ b/openbb_terminal/portfolio/portfolio_view.py @@ -145,6 +145,7 @@ def display_assets_allocation( portfolio_engine: PortfolioEngine, limit: int = 10, tables: bool = False, + recalculate: bool = False, ): """Display portfolio asset allocation compared to the benchmark @@ -152,16 +153,19 @@ def display_assets_allocation( ---------- portfolio_engine: PortfolioEngine Instance of PortfolioEngine class - tables: bool - Whether to include separate asset allocation tables limit: int The amount of assets you wish to show, by default this is set to 10 + tables: bool + Whether to include separate asset allocation tables + recalculate: bool + Whether to recalculate the allocation """ combined, portfolio_allocation, benchmark_allocation = get_assets_allocation( portfolio_engine=portfolio_engine, limit=limit, tables=True, + recalculate=recalculate, ) display_category( @@ -179,6 +183,7 @@ def display_sectors_allocation( portfolio_engine: PortfolioEngine, limit: int = 10, tables: bool = False, + recalculate: bool = False, ): """Display portfolio sector allocation compared to the benchmark @@ -190,12 +195,15 @@ def display_sectors_allocation( The amount of assets you wish to show, by default this is set to 10 tables: bool Whether to include separate asset allocation tables + recalculate: bool + Whether to recalculate the allocation """ combined, portfolio_allocation, benchmark_allocation = get_sectors_allocation( portfolio_engine=portfolio_engine, limit=limit, tables=True, + recalculate=recalculate, ) display_category( @@ -213,6 +221,7 @@ def display_countries_allocation( portfolio_engine: PortfolioEngine, limit: int = 10, tables: bool = False, + recalculate: bool = False, ): """Display portfolio country allocation compared to the benchmark @@ -224,12 +233,15 @@ def display_countries_allocation( The amount of assets you wish to show, by default this is set to 10 tables: bool Whether to include separate asset allocation tables + recalculate: bool + Whether to recalculate the allocation """ combined, portfolio_allocation, benchmark_allocation = get_countries_allocation( portfolio_engine=portfolio_engine, limit=limit, tables=True, + recalculate=recalculate, ) display_category( @@ -247,6 +259,7 @@ def display_regions_allocation( portfolio_engine: PortfolioEngine, limit: int = 10, tables: bool = False, + recalculate: bool = False, ): """Display portfolio region allocation compared to the benchmark @@ -258,12 +271,15 @@ def display_regions_allocation( The amount of assets you wish to show, by default this is set to 10 tables: bool Whether to include separate asset allocation tables + recalculate: bool + Whether to recalculate the allocation """ combined, portfolio_allocation, benchmark_allocation = get_regions_allocation( portfolio_engine=portfolio_engine, limit=limit, tables=True, + recalculate=recalculate, ) display_category( From 3f9fe8b685f3890b9971512ea3493843e622a08d Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Sun, 5 Feb 2023 13:22:02 +0000 Subject: [PATCH 06/12] lower before compare --- openbb_terminal/core/completer/choices.py | 6 +++--- openbb_terminal/decorators.py | 2 +- openbb_terminal/portfolio/portfolio_engine.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openbb_terminal/core/completer/choices.py b/openbb_terminal/core/completer/choices.py index ba9ffbed834..6d3a58dc01a 100644 --- a/openbb_terminal/core/completer/choices.py +++ b/openbb_terminal/core/completer/choices.py @@ -214,7 +214,7 @@ def __patch_controller_functions(controller): ), ] - if environ.get("DEBUG_MODE", "false") != "true": + if str(environ.get("DEBUG_MODE", "false")).lower() != "true": rich.start() patched_function_list = [] for patcher in patcher_list: @@ -222,7 +222,7 @@ def __patch_controller_functions(controller): yield patched_function_list - if environ.get("DEBUG_MODE", "false") != "true": + if str(environ.get("DEBUG_MODE", "false")).lower() != "true": rich.stop() for patcher in patcher_list: patcher.stop() @@ -319,7 +319,7 @@ def build_controller_choice_map(controller) -> dict: argument_parser=argument_parser ) except Exception as exception: - if environ.get("DEBUG_MODE", "false") == "true": + if str(environ.get("DEBUG_MODE", "false")).lower() == "true": raise Exception( f"On command : `{command}`.\n{str(exception)}" ) from exception diff --git a/openbb_terminal/decorators.py b/openbb_terminal/decorators.py index f94cbb0227b..9ced3603f06 100644 --- a/openbb_terminal/decorators.py +++ b/openbb_terminal/decorators.py @@ -60,7 +60,7 @@ def log_start_end(func=None, log=None): extra={"func_name_override": func.__name__}, ) - if os.environ.get("DEBUG_MODE") == "true": + if str(os.environ.get("DEBUG_MODE")).lower() == "true": value = func(*args, **kwargs) log.info("END", extra={"func_name_override": func.__name__}) return value diff --git a/openbb_terminal/portfolio/portfolio_engine.py b/openbb_terminal/portfolio/portfolio_engine.py index 8a4357cec06..22d33b552a2 100644 --- a/openbb_terminal/portfolio/portfolio_engine.py +++ b/openbb_terminal/portfolio/portfolio_engine.py @@ -195,7 +195,7 @@ class PortfolioEngine: # Load transactions from file if path.endswith(".xlsx"): - if environ.get("DEBUG_MODE", "false") != "true": + if str(environ.get("DEBUG_MODE", "false")).lower() != "true": warnings.filterwarnings( "ignore", category=UserWarning, module="openpyxl" ) From 53bab3962d995a5fe3114921fb46f4b5dd685114 Mon Sep 17 00:00:00 2001 From: jeroen Date: Mon, 6 Feb 2023 13:28:51 +0100 Subject: [PATCH 07/12] Add sheetnames and improve mret --- .../portfolio/portfolio_controller.py | 107 ++++++++++++++---- openbb_terminal/portfolio/portfolio_model.py | 31 ++++- openbb_terminal/portfolio/portfolio_view.py | 76 ++++++++++--- 3 files changed, 177 insertions(+), 37 deletions(-) diff --git a/openbb_terminal/portfolio/portfolio_controller.py b/openbb_terminal/portfolio/portfolio_controller.py index 3551fe12b57..ae29a5e7df2 100644 --- a/openbb_terminal/portfolio/portfolio_controller.py +++ b/openbb_terminal/portfolio/portfolio_controller.py @@ -968,6 +968,23 @@ class PortfolioController(BaseController): help="Period to select start end of the year returns", metavar="PERIOD", ) + parser.add_argument( + "-i", + "--instrument", + type=str, + dest="instrument", + default="both", + choices=["both", "portfolio", "benchmark"], + help="Whether to show portfolio or benchmark monthly returns. By default both are shown in one table.", + ) + parser.add_argument( + "-g", + "--graph", + action="store_true", + default=False, + dest="graph", + help="Plot the monthly returns on a heatmap", + ) parser.add_argument( "-s", "--show", @@ -982,7 +999,7 @@ class PortfolioController(BaseController): parser, other_args, raw=True, - export_allowed=EXPORT_ONLY_FIGURES_ALLOWED, + export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED, ) if ns_parser and self.portfolio is not None: @@ -992,10 +1009,13 @@ class PortfolioController(BaseController): portfolio_view.display_monthly_returns( self.portfolio, ns_parser.period, - ns_parser.raw, + ns_parser.instrument, + ns_parser.graph, ns_parser.show_vals, ns_parser.export, - ns_parser.sheet_name, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, ) @log_start_end(log=logger) @@ -1037,7 +1057,9 @@ class PortfolioController(BaseController): ns_parser.raw, ns_parser.limit, ns_parser.export, - ns_parser.sheet_name, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, ) @log_start_end(log=logger) @@ -1059,9 +1081,6 @@ class PortfolioController(BaseController): portfolio_view.display_maximum_drawdown( self.portfolio, export=ns_parser.export, - sheet_name=" ".join(ns_parser.sheet_name) - if ns_parser.sheet_name - else None, ) @log_start_end(log=logger) @@ -1295,18 +1314,26 @@ class PortfolioController(BaseController): self.portfolio, ns_parser.risk_free_rate / 100, ns_parser.export, - ns_parser.sheet_name, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, ) elif ns_parser.metric == "sortino": portfolio_view.display_sortino_ratio( self.portfolio, ns_parser.risk_free_rate / 100, ns_parser.export, - ns_parser.sheet_name, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, ) elif ns_parser.metric == "maxdrawdown": portfolio_view.display_maximum_drawdown_ratio( - self.portfolio, ns_parser.export, ns_parser.sheet_name + self.portfolio, + ns_parser.export, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, ) elif ns_parser.metric == "rsquare": portfolio_view.display_rsquare( @@ -1318,15 +1345,27 @@ class PortfolioController(BaseController): ) elif ns_parser.metric == "gaintopain": portfolio_view.display_gaintopain_ratio( - self.portfolio, ns_parser.export, ns_parser.sheet_name + self.portfolio, + ns_parser.export, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, ) elif ns_parser.metric == "trackerr": portfolio_view.display_tracking_error( - self.portfolio, ns_parser.export, ns_parser.sheet_name + self.portfolio, + ns_parser.export, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, ) elif ns_parser.metric == "information": portfolio_view.display_information_ratio( - self.portfolio, ns_parser.export, ns_parser.sheet_name + self.portfolio, + ns_parser.export, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, ) elif ns_parser.metric == "tail": portfolio_view.display_tail_ratio( @@ -1338,30 +1377,52 @@ class PortfolioController(BaseController): ) elif ns_parser.metric == "commonsense": portfolio_view.display_common_sense_ratio( - self.portfolio, ns_parser.export, ns_parser.sheet_name + self.portfolio, + ns_parser.export, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, ) elif ns_parser.metric == "jensens": portfolio_view.display_jensens_alpha( self.portfolio, ns_parser.risk_free_rate / 100, ns_parser.export, - ns_parser.sheet_name, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, ) elif ns_parser.metric == "calmar": portfolio_view.display_calmar_ratio( - self.portfolio, ns_parser.export, ns_parser.sheet_name + self.portfolio, + ns_parser.export, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, ) elif ns_parser.metric == "kelly": portfolio_view.display_kelly_criterion( - self.portfolio, ns_parser.export, ns_parser.sheet_name + self.portfolio, + ns_parser.export, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, ) elif ns_parser.metric == "payoff" and self.portfolio is not None: portfolio_view.display_payoff_ratio( - self.portfolio, ns_parser.export, ns_parser.sheet_name + self.portfolio, + ns_parser.export, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, ) elif ns_parser.metric == "profitfactor" and self.portfolio is not None: portfolio_view.display_profit_factor( - self.portfolio, ns_parser.export, ns_parser.sheet_name + self.portfolio, + ns_parser.export, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, ) @log_start_end(log=logger) @@ -1401,7 +1462,9 @@ class PortfolioController(BaseController): ns_parser.period, ns_parser.raw, ns_parser.export, - ns_parser.sheet_name, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, ) @log_start_end(log=logger) @@ -1448,7 +1511,9 @@ class PortfolioController(BaseController): ns_parser.period, ns_parser.risk_free_rate / 100, ns_parser.export, - ns_parser.sheet_name, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, ) diff --git a/openbb_terminal/portfolio/portfolio_model.py b/openbb_terminal/portfolio/portfolio_model.py index b5292d41888..030cf692095 100644 --- a/openbb_terminal/portfolio/portfolio_model.py +++ b/openbb_terminal/portfolio/portfolio_model.py @@ -371,7 +371,36 @@ def get_monthly_returns( ], ) - return monthly_returns, bench_monthly_returns + years = [ + (year, instrument) + for year in monthly_returns.index + for instrument in ["Portfolio", "Benchmark", "Alpha"] + ] + total_monthly_returns = pd.DataFrame( + np.nan, + columns=monthly_returns.columns, + index=pd.MultiIndex.from_tuples(years, names=["Year", "Instrument"]), + ) + + for year in monthly_returns.index: + for instrument in ["Portfolio", "Benchmark", "Alpha"]: + if instrument == "Portfolio": + total_monthly_returns.loc[ + (year, instrument), monthly_returns.columns + ] = monthly_returns.loc[year].values + elif instrument == "Benchmark": + total_monthly_returns.loc[ + (year, instrument), monthly_returns.columns + ] = bench_monthly_returns.loc[year].values + elif instrument == "Alpha": + total_monthly_returns.loc[ + (year, instrument), monthly_returns.columns + ] = ( + monthly_returns.loc[year].values + - bench_monthly_returns.loc[year].values + ) + + return monthly_returns, bench_monthly_returns, total_monthly_returns @log_start_end(log=logger) diff --git a/openbb_terminal/portfolio/portfolio_view.py b/openbb_terminal/portfolio/portfolio_view.py index 132ca92a484..ad93590837c 100644 --- a/openbb_terminal/portfolio/portfolio_view.py +++ b/openbb_terminal/portfolio/portfolio_view.py @@ -475,7 +475,8 @@ def display_yearly_returns( def display_monthly_returns( portfolio_engine: PortfolioEngine, window: str = "all", - raw: bool = False, + instrument: str = "both", + graph: bool = False, show_vals: bool = False, export: str = "", sheet_name: str = None, @@ -490,27 +491,38 @@ def display_monthly_returns( Use `portfolio.load` to create a PortfolioEngine. window : str interval to compare cumulative returns and benchmark - raw : False - Display raw data from cumulative return + instrument : str + Display raw data from cumulative return, default is showing both the portfolio and benchmark in one table show_vals : False Show values on heatmap export : str Export certain type of data + sheet_name : str + Whether to export to a specific sheet name in an Excel file external_axes: plt.Axes Optional axes to display plot on """ - portfolio_returns, benchmark_returns = get_monthly_returns(portfolio_engine, window) + portfolio_returns, benchmark_returns, total_monthly_returns = get_monthly_returns( + portfolio_engine, window + ) - if raw: + if instrument == "portfolio": print_rich_table( portfolio_returns, title="Monthly returns - portfolio [%]", headers=portfolio_returns.columns, show_index=True, ) - console.print("\n") + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "mret_portfolio", + portfolio_returns, + sheet_name, + ) + elif instrument == "benchmark": print_rich_table( benchmark_returns, title="Monthly returns - benchmark [%]", @@ -518,7 +530,49 @@ def display_monthly_returns( show_index=True, ) - else: + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "mret_benchmark", + benchmark_returns, + sheet_name, + ) + elif instrument == "both": + # Converts multi-index into readable rich table while keeping the multi-index intact for export + total_monthly_returns_rich = total_monthly_returns.copy() + total_monthly_returns_rich.insert( + 0, "Instruments", total_monthly_returns_rich.index.get_level_values(1) + ) + total_monthly_returns_rich.insert( + 0, + "Year", + sum( + [ + [year, "", ""] + for year in total_monthly_returns_rich.index.get_level_values( + 0 + ).unique() + ], + [], + ), + ) + + print_rich_table( + total_monthly_returns_rich, + title="Total Monthly returns [%]", + headers=total_monthly_returns_rich.columns, + show_index=False, + ) + + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "mret_total", + total_monthly_returns, + sheet_name, + ) + + if graph or show_vals: if external_axes is None: _, ax = plt.subplots( 2, @@ -564,14 +618,6 @@ def display_monthly_returns( if external_axes is None: theme.visualize_output() - export_data( - export, - os.path.dirname(os.path.abspath(__file__)), - "mret", - portfolio_returns, - sheet_name, - ) - @log_start_end(log=logger) def display_daily_returns( From 7dea88b43eb4fb84bbae739c3d699b1d93cbac56 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Mon, 6 Feb 2023 13:51:36 +0000 Subject: [PATCH 08/12] add test case --- .../portfolio/test_portfolio.openbb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openbb_terminal/miscellaneous/integration_tests_scripts/portfolio/test_portfolio.openbb b/openbb_terminal/miscellaneous/integration_tests_scripts/portfolio/test_portfolio.openbb index 572a3955c5b..46f17097e51 100644 --- a/openbb_terminal/miscellaneous/integration_tests_scripts/portfolio/test_portfolio.openbb +++ b/openbb_terminal/miscellaneous/integration_tests_scripts/portfolio/test_portfolio.openbb @@ -8,6 +8,11 @@ alloc assets -t -l 4 alloc sectors -t -l 4 alloc countries -t -l 4 alloc regions -t -l 4 +bench SPY +alloc assets -t -l 4 +alloc sectors -t -l 4 +alloc countries -t -l 4 +alloc regions -t -l 4 attrib attrib --raw attrib --type absolute From 867277fe01a22e1acb1bd1f118e9f4d53a6dbf3e Mon Sep 17 00:00:00 2001 From: montezdesousa <79287829+montezdesousa@users.noreply.github.com> Date: Mon, 6 Feb 2023 15:38:38 +0000 Subject: [PATCH 09/12] lower before compare (#4133) --- openbb_terminal/core/completer/choices.py | 6 +++--- openbb_terminal/decorators.py | 2 +- openbb_terminal/portfolio/portfolio_engine.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openbb_terminal/core/completer/choices.py b/openbb_terminal/core/completer/choices.py index ba9ffbed834..6d3a58dc01a 100644 --- a/openbb_terminal/core/completer/choices.py +++ b/openbb_terminal/core/completer/choices.py @@ -214,7 +214,7 @@ def __patch_controller_functions(controller): ), ] - if environ.get("DEBUG_MODE", "false") != "true": + if str(environ.get("DEBUG_MODE", "false")).lower() != "true": rich.start() patched_function_list = [] for patcher in patcher_list: @@ -222,7 +222,7 @@ def __patch_controller_functions(controller): yield patched_function_list - if environ.get("DEBUG_MODE", "false") != "true": + if str(environ.get("DEBUG_MODE", "false")).lower() != "true": rich.stop() for patcher in patcher_list: patcher.stop() @@ -319,7 +319,7 @@ def build_controller_choice_map(controller) -> dict: argument_parser=argument_parser ) except Exception as exception: - if environ.get("DEBUG_MODE", "false") == "true": + if str(environ.get("DEBUG_MODE", "false")).lower() == "true": raise Exception( f"On command : `{command}`.\n{str(exception)}" ) from exception diff --git a/openbb_terminal/decorators.py b/openbb_terminal/decorators.py index f94cbb0227b..9ced3603f06 100644 --- a/openbb_terminal/decorators.py +++ b/openbb_terminal/decorators.py @@ -60,7 +60,7 @@ def log_start_end(func=None, log=None): extra={"func_name_override": func.__name__}, ) - if os.environ.get("DEBUG_MODE") == "true": + if str(os.environ.get("DEBUG_MODE")).lower() == "true": value = func(*args, **kwargs) log.info("END", extra={"func_name_override": func.__name__}) return value diff --git a/openbb_terminal/portfolio/portfolio_engine.py b/openbb_terminal/portfolio/portfolio_engine.py index 8a4357cec06..22d33b552a2 100644 --- a/openbb_terminal/portfolio/portfolio_engine.py +++ b/openbb_terminal/portfolio/portfolio_engine.py @@ -195,7 +195,7 @@ class PortfolioEngine: # Load transactions from file if path.endswith(".xlsx"): - if environ.get("DEBUG_MODE", "false") != "true": + if str(environ.get("DEBUG_MODE", "false")).lower() != "true": warnings.filterwarnings( "ignore", category=UserWarning, module="openpyxl" ) From bd0338f71965b276c055b5555272b178594eead8 Mon Sep 17 00:00:00 2001 From: montezdesousa <79287829+montezdesousa@users.noreply.github.com> Date: Mon, 6 Feb 2023 15:45:15 +0000 Subject: [PATCH 10/12] lower before compare (#4133) From f8f13e25df532ad102e6f56b400e96572c39f06a Mon Sep 17 00:00:00 2001 From: Jeroen Bouma Date: Mon, 6 Feb 2023 18:05:40 +0100 Subject: [PATCH 11/12] Fix portfolio (#4147) Co-authored-by: montezdesousa <79287829+montezdesousa@users.noreply.github.com> --- openbb_terminal/portfolio/portfolio_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/portfolio/portfolio_engine.py b/openbb_terminal/portfolio/portfolio_engine.py index 22d33b552a2..bdad5ef64f5 100644 --- a/openbb_terminal/portfolio/portfolio_engine.py +++ b/openbb_terminal/portfolio/portfolio_engine.py @@ -695,7 +695,7 @@ class PortfolioEngine: # Make historical prices columns a multi-index. This helps the merging. self.portfolio_historical_prices.columns = pd.MultiIndex.from_product( - [["Close"], self.tickers_list] + [["Close"], self.portfolio_historical_prices.columns] ) trade_data = pd.merge( From ac1a3bd64a3376921a568c2e85f5884269d36818 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Mon, 6 Feb 2023 17:27:11 +0000 Subject: [PATCH 12/12] fix: upgrade tailwindcss-radix from 2.6.1 to 2.6.2 Snyk has created this PR to upgrade tailwindcss-radix from 2.6.1 to 2.6.2. See this package in npm: https://www.npmjs.com/package/tailwindcss-radix See this project in Snyk: https://app.snyk.io/org/openbb-terminal-shared/project/026f18ab-2849-452c-aada-400073ca5ee2?utm_source=github&utm_medium=referral&page=upgrade-pr --- website/package-lock.json | 16 ++++++++-------- website/package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/website/package-lock.json b/website/package-lock.json index 126ab3b24ed..3be7359d913 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -12,14 +12,14 @@ "@docusaurus/core": "2.2.0", "@docusaurus/plugin-client-redirects": "^2.3.1", "@docusaurus/plugin-content-docs": "^2.2.0", - "@docusaurus/preset-classic": "^2.3.1", + "@docusaurus/preset-classic": "2.3.1", "@mdx-js/react": "^1.6.22", "@radix-ui/react-popover": "^1.0.2", "clsx": "^1.2.1", "prism-react-renderer": "^1.3.5", "react": "^17.0.2", "react-dom": "^17.0.2", - "tailwindcss-radix": "^2.6.1" + "tailwindcss-radix": "^2.6.2" }, "devDependencies": { "@docusaurus/module-type-aliases": "2.2.0", @@ -14821,9 +14821,9 @@ } }, "node_modules/tailwindcss-radix": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/tailwindcss-radix/-/tailwindcss-radix-2.6.1.tgz", - "integrity": "sha512-ejh4BHkn/3g349l+JiEW6I4//uclU4nllEqnOK3/8zpORscIk9WbjLyYR2UzqwVRjjzgd72I/hMDJZm3+XO48w==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tailwindcss-radix/-/tailwindcss-radix-2.6.2.tgz", + "integrity": "sha512-+pqIyqY9mjMXtJHyDlkiJYMfpa8+Gkt2UE6VrmbYjbLAqva7Lg4q2Rffe9JFNv9dBFfoDTVML7gQd4BDJXOrCg==" }, "node_modules/tailwindcss/node_modules/glob-parent": { "version": "6.0.2", @@ -27161,9 +27161,9 @@ } }, "tailwindcss-radix": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/tailwindcss-radix/-/tailwindcss-radix-2.6.1.tgz", - "integrity": "sha512-ejh4BHkn/3g349l+JiEW6I4//uclU4nllEqnOK3/8zpORscIk9WbjLyYR2UzqwVRjjzgd72I/hMDJZm3+XO48w==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tailwindcss-radix/-/tailwindcss-radix-2.6.2.tgz", + "integrity": "sha512-+pqIyqY9mjMXtJHyDlkiJYMfpa8+Gkt2UE6VrmbYjbLAqva7Lg4q2Rffe9JFNv9dBFfoDTVML7gQd4BDJXOrCg==" }, "tapable": { "version": "2.2.1", diff --git a/website/package.json b/website/package.json index b6718701c34..9ad4eb560b2 100644 --- a/website/package.json +++ b/website/package.json @@ -27,7 +27,7 @@ "prism-react-renderer": "^1.3.5", "react": "^17.0.2", "react-dom": "^17.0.2", - "tailwindcss-radix": "^2.6.1" + "tailwindcss-radix": "^2.6.2" }, "devDependencies": { "@docusaurus/module-type-aliases": "2.2.0",