diff --git a/openbb_terminal/portfolio/portfolio_controller.py b/openbb_terminal/portfolio/portfolio_controller.py index 684b351fada..41f1e40e08b 100644 --- a/openbb_terminal/portfolio/portfolio_controller.py +++ b/openbb_terminal/portfolio/portfolio_controller.py @@ -1004,6 +1004,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", @@ -1018,7 +1035,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: @@ -1028,10 +1045,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) @@ -1073,7 +1093,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) @@ -1095,9 +1117,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) @@ -1331,18 +1350,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( @@ -1354,15 +1381,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( @@ -1374,30 +1413,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) @@ -1437,7 +1498,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) @@ -1484,7 +1547,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 289423360b7..c4fc1fdee41 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(