Skip to content

Commit

Permalink
nCrResult update
Browse files Browse the repository at this point in the history
HTML reporting capability implemented.
  • Loading branch information
GongJr0 committed Jan 3, 2025
1 parent 57dfbe8 commit afeac9b
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 95 deletions.
94 changes: 1 addition & 93 deletions NeoPortfolio/CustomTypes.py
Original file line number Diff line number Diff line change
@@ -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"""
<h2>Portfolio Analysis</h2>
<h3>Summary Metrics</h3>
<ul>
<li><strong>Expected Portfolio Return:</strong> {portfolio_dict['return'] * 100:.4f}%</li>
<li><strong>Portfolio Variance:</strong> {portfolio_dict['portfolio_variance'] * 100:.4f}%</li>
</ul>
<h3>Portfolio Composition</h3>
{portfolio_html}
<h3>Covariance Matrix</h3>
{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()
15 changes: 13 additions & 2 deletions NeoPortfolio/nCrOptimize.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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)

Expand Down
166 changes: 166 additions & 0 deletions NeoPortfolio/nCrResult.py
Original file line number Diff line number Diff line change
@@ -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"""
<h2>Portfolio Analysis</h2>
<h3>Summary Metrics</h3>
<ul>
<li><strong>Expected Portfolio Return:</strong> {portfolio_dict['return'] * 100:.4f}%</li>
<li><strong>Portfolio Variance:</strong> {portfolio_dict['portfolio_variance'] * 100:.4f}%</li>
</ul>
<h3>Portfolio Composition</h3>
{portfolio_html}
<h3>Covariance Matrix</h3>
{cov}
<h3>Optimized Combination Space</h3>
"""

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()

0 comments on commit afeac9b

Please sign in to comment.