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
-
- - Expected Portfolio Return: {portfolio_dict['return'] * 100:.4f}%
- - Portfolio Variance: {portfolio_dict['portfolio_variance'] * 100:.4f}%
-
- 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
+
+ - Expected Portfolio Return: {portfolio_dict['return'] * 100:.4f}%
+ - Portfolio Variance: {portfolio_dict['portfolio_variance'] * 100:.4f}%
+
+ 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