From afeac9baf2a48b29dee1d520480b47d09228f9c1 Mon Sep 17 00:00:00 2001 From: GongJr0 Date: Fri, 3 Jan 2025 17:15:34 +0100 Subject: [PATCH] `nCrResult` update HTML reporting capability implemented. --- NeoPortfolio/CustomTypes.py | 94 +------------------- NeoPortfolio/nCrOptimize.py | 15 +++- NeoPortfolio/nCrResult.py | 166 ++++++++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 95 deletions(-) create mode 100644 NeoPortfolio/nCrResult.py diff --git a/NeoPortfolio/CustomTypes.py b/NeoPortfolio/CustomTypes.py index 2826838..3599717 100644 --- a/NeoPortfolio/CustomTypes.py +++ b/NeoPortfolio/CustomTypes.py @@ -1,99 +1,7 @@ from typing import NewType, Literal, Tuple, Union -from IPython.display import HTML -import pandas as pd +# Type aliases StockSymbol = str StockDataSubset = Tuple[Literal['Open', 'High', 'Low', 'Close', 'Volume', 'Dividends', 'Stock Splits']] Days = int - - -class nCrResult(list): - """ - Class to store the result of a nCrEngine calculation. - """ - def __init__(self, *args): - super().__init__(*args) - - @staticmethod - def beautify_portfolio(portfolio_dict): - portfolio = portfolio_dict['portfolio'].split(' - ') - weights = portfolio_dict['weights'] - expected_returns = portfolio_dict['expected_returns'] - cov_matrix = portfolio_dict['cov_matrix'] - betas = portfolio_dict['betas'] - - # Create a DataFrame for the portfolio and weights - portfolio_df = pd.DataFrame({ - "Weight": weights, - "Expected Return": expected_returns, - "Beta": betas - }, index=portfolio) - - # Generate HTML for the table - portfolio_html = portfolio_df.to_html(float_format="%.4f") - cov = pd.DataFrame(cov_matrix, index=portfolio, columns=portfolio).to_html(float_format="%.4f") - - # Create additional HTML content for metrics - summary_html = f""" -

Portfolio Analysis

-

Summary Metrics

- -

Portfolio Composition

- {portfolio_html} - -

Covariance Matrix

- {cov} - """ - - # Combine all HTML content - full_html = summary_html - - return HTML(full_html) - - def _best_portfolio(self) -> HTML | dict: - return max( - self, - key=lambda x: x['return'] / x['portfolio_variance'] - ) - - def _max_return(self, display: bool = False) -> dict: - if display: - return self.beautify_portfolio(self.max_return()) - return max( - self, - key=lambda x: x['return'] - ) - - def _min_volatility(self) -> dict: - return min( - self, - key=lambda x: x['portfolio_variance'] - ) - - def max_return(self, display: bool = False) -> dict | HTML: - """ - Get the maximum return from the result. - """ - if display: - return self.beautify_portfolio(self._max_return()) - return self._max_return() - - def min_volatility(self, display: bool = False) -> dict | HTML: - """ - Get the minimum volatility from the result. - """ - if display: - return self.beautify_portfolio(self._min_volatility()) - return self._min_volatility() - - def best_portfolio(self, display: bool = False) -> dict | HTML: - """ - Get the best portfolio from the result. - """ - if display: - return self.beautify_portfolio(self._best_portfolio()) - return self._best_portfolio() \ No newline at end of file diff --git a/NeoPortfolio/nCrOptimize.py b/NeoPortfolio/nCrOptimize.py index f7d7013..28189ae 100644 --- a/NeoPortfolio/nCrOptimize.py +++ b/NeoPortfolio/nCrOptimize.py @@ -3,7 +3,7 @@ from .nCrEngine import nCrEngine from .Sentiment import Sentiment from .PortfolioCache import PortfolioCache -from .CustomTypes import nCrResult +from .nCrResult import nCrResult import datetime as dt @@ -44,6 +44,8 @@ def __init__(self, self.market_returns = self._get_market().tz_localize(None) + self.rf_rate = self._get_rf_rate() + def _get_portfolios(self) -> list: """ Get Portfolio objects from string combinations. @@ -66,6 +68,14 @@ def _get_market(self) -> pd.Series: return market_returns + def _get_rf_rate(self) -> float: + """Get the risk-free rate for the given horizon.""" + ticker = yf.Ticker("^TNX") + pa_rate = ticker.history(period="1d")["Close"].iloc[0] / 100 + + horizon_rate = (1 + pa_rate/2)**(2*(self.horizon/360)) - 1 # Semi-annual compounding with ACT/360 convention + return horizon_rate + def _iteration_optimize(self, portfolio, bounds: tuple[float, float] = (0.0, 1.0)) -> dict[str, Any]: """Optimization function ran in parallel iteration of portfolios. @@ -141,7 +151,8 @@ def optimize_space(self, bounds: tuple = (0.0, 1.0)) -> nCrResult: :return: List of optimized portfolios (best to worst) """ results = nCrResult([self._iteration_optimize(portfolio, bounds) - for portfolio in tqdm(self.portfolios, desc="Iterating over portfolios")]) + for portfolio in tqdm(self.portfolios, desc="Iterating over portfolios")], + rf_rate=self.rf_rate) results.sort(key=lambda x: (x['return'], -x['portfolio_variance']), reverse=True) diff --git a/NeoPortfolio/nCrResult.py b/NeoPortfolio/nCrResult.py new file mode 100644 index 0000000..78ae76c --- /dev/null +++ b/NeoPortfolio/nCrResult.py @@ -0,0 +1,166 @@ +import numpy as np +from IPython.display import HTML, display +import pandas as pd +import plotly.express as px + +class nCrResult(list): + """ + Class to store the result of a nCrEngine calculation. + """ + + def __init__(self, *args, rf_rate: float): + super().__init__(*args) + self.horizon_rf_rate = rf_rate + + + def _beautify_portfolio(self, portfolio_dict): + portfolio = portfolio_dict['portfolio'].split(' - ') + weights = portfolio_dict['weights'] + expected_returns = portfolio_dict['expected_returns'] + cov_matrix = portfolio_dict['cov_matrix'] + betas = portfolio_dict['betas'] + + # Create a DataFrame for the portfolio and weights + portfolio_df = pd.DataFrame({ + "Weight": weights, + "Expected Return": expected_returns, + "Beta": betas + }, index=portfolio) + + # Generate HTML for the table + portfolio_html = portfolio_df.to_html(float_format="%.4f") + cov = pd.DataFrame(cov_matrix, index=portfolio, columns=portfolio).to_html(float_format="%.4f") + + # Create plotly scatter of all portfolios + all_portfolios = { + 'Portfolio': [x['portfolio'] for x in self], + 'Return': [x['return'] for x in self], + 'Variance': [x['portfolio_variance'] for x in self] + } + + df = pd.DataFrame(all_portfolios) + df.set_index('Portfolio', inplace=True) + + df['Sharpe Ratio'] = (df['Return'] - self.horizon_rf_rate) / np.sqrt(df['Variance']) + + max_return = df.loc[df['Return'].idxmax(), ['Return', 'Variance', 'Sharpe Ratio']] + min_volatility = df.loc[df['Variance'].idxmin(), ['Return', 'Variance', 'Sharpe Ratio']] + best_portfolio = df.loc[(df['Return'] / df['Variance']).idxmax(), ['Return', 'Variance', 'Sharpe Ratio']] + + fig = px.scatter(df, + x='Variance', + y='Return', + color='Sharpe Ratio', + color_continuous_scale='Viridis', + title='Optimized Portfolios') + + # Add traces for each key portfolio with proper colors and symbols + fig.add_trace( + px.scatter( + x=[max_return['Variance']], y=[max_return['Return']], + size=[10], symbol=[f'Max Return: {max_return.name}'], labels={'x': 'Variance', 'y': 'Return'}, + ).data[0] + ) + fig.data[-1].marker.color = 'red' + fig.data[-1].marker.size = 10 + + fig.add_trace( + px.scatter( + x=[min_volatility['Variance']], y=[min_volatility['Return']], + size=[10], symbol=[f'Min Volatility: {min_volatility.name}'], labels={'x': 'Variance', 'y': 'Return'}, + ).data[0] + ) + fig.data[-1].marker.color = 'green' + fig.data[-1].marker.size = 10 + + fig.add_trace( + px.scatter( + x=[best_portfolio['Variance']], y=[best_portfolio['Return']], + size=[10], symbol=[f'Best Portfolio: {best_portfolio.name}'], labels={'x': 'Variance', 'y': 'Return'}, + ).data[0] + ) + fig.data[-1].marker.color = 'violet' + fig.data[-1].marker.size = 10 + + # Update layout to improve legend placement and color scale + fig.update_layout( + xaxis_title='Variance', + yaxis_title='Return', + title='Optimized Portfolios', + coloraxis_colorbar=dict( + title='Sharpe Ratio', + tickvals=[min(df['Sharpe Ratio']), max(df['Sharpe Ratio'])], + ticktext=[f'{min(df["Sharpe Ratio"]):.2f}', f'{max(df["Sharpe Ratio"]):.2f}'], + ), + legend=dict( + orientation="h", + yanchor="bottom", + y=1.1, + xanchor="right", + x=0.5 + ) + ) + + # Create additional HTML content for metrics + summary_html = f""" +

Portfolio Analysis

+

Summary Metrics

+ +

Portfolio Composition

+ {portfolio_html} + +

Covariance Matrix

+ {cov} + +

Optimized Combination Space

+ """ + + display(HTML(summary_html)) + fig.show() + + def _best_portfolio(self) -> HTML | dict: + return max( + self, + key=lambda x: x['return'] / x['portfolio_variance'] + ) + + def _max_return(self, display: bool = False) -> dict: + if display: + return self.beautify_portfolio(self.max_return()) + return max( + self, + key=lambda x: x['return'] + ) + + def _min_volatility(self) -> dict: + return min( + self, + key=lambda x: x['portfolio_variance'] + ) + + def max_return(self, display: bool = False) -> dict | HTML: + """ + Get the maximum return from the result. + """ + if display: + return self._beautify_portfolio(self._max_return()) + return self._max_return() + + def min_volatility(self, display: bool = False) -> dict | HTML: + """ + Get the minimum volatility from the result. + """ + if display: + return self._beautify_portfolio(self._min_volatility()) + return self._min_volatility() + + def best_portfolio(self, display: bool = False) -> dict | HTML: + """ + Get the best portfolio from the result. + """ + if display: + return self._beautify_portfolio(self._best_portfolio()) + return self._best_portfolio() \ No newline at end of file