Skip to content

Commit

Permalink
fix(portfolio): add effective number of assets
Browse files Browse the repository at this point in the history
  • Loading branch information
HugoDelatte committed Feb 4, 2024
1 parent 3b4dcf1 commit f4dbc5f
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 12 deletions.
1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Functions
measures.ulcer_index
measures.gini_mean_difference
measures.owa_gmd_weights
measures.effective_number_assets

.. _portfolio_ref:

Expand Down
2 changes: 2 additions & 0 deletions src/skfolio/measures/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
cvar,
drawdown_at_risk,
edar,
effective_number_assets,
entropic_risk_measure,
evar,
first_lower_partial_moment,
Expand Down Expand Up @@ -73,4 +74,5 @@
"owa_gmd_weights",
"skew",
"kurtosis",
"effective_number_assets",
]
8 changes: 5 additions & 3 deletions src/skfolio/measures/_measures.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,11 +611,13 @@ def gini_mean_difference(returns: np.ndarray) -> float:


def effective_number_assets(weights: np.ndarray) -> float:
r"""
Computes the effective number of assets, defined as the inverse of the Herfindahl index [1]_:
r"""Computes the effective number of assets, defined as the inverse of the
Herfindahl index [1]_:
.. math:: N_{eff} = \frac{1}{\Vert w \Vert_{2}^{2}}
It quantifies portfolio concentration, with a higher value indicating a more diversified portfolio.
It quantifies portfolio concentration, with a higher value indicating a more
diversified portfolio.
Parameters
----------
Expand Down
31 changes: 28 additions & 3 deletions src/skfolio/portfolio/_portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import plotly.express as px

import skfolio.typing as skt
from skfolio.measures import RiskMeasure
from skfolio.measures import RiskMeasure, effective_number_assets
from skfolio.portfolio._base import _ZERO_THRESHOLD, BasePortfolio
from skfolio.utils.tools import (
args_names,
Expand Down Expand Up @@ -691,6 +691,28 @@ def sric(self) -> float:
self.n_observations * self.sharpe_ratio
)

@property
def effective_number_assets(self) -> float:
r"""Computes the effective number of assets, defined as the inverse of the
Herfindahl index [1]_:
.. math:: N_{eff} = \frac{1}{\Vert w \Vert_{2}^{2}}
It quantifies portfolio concentration, with a higher value indicating a more
diversified portfolio.
Returns
-------
value : float
Effective number of assets.
References
----------
.. [1] "Banking and Financial Institutions Law in a Nutshell".
Lovett, William Anthony (1988)
"""
return effective_number_assets(weights=self.weights)

# Public methods
def expected_returns_from_assets(
self, assets_expected_returns: np.ndarray
Expand Down Expand Up @@ -817,9 +839,12 @@ def summary(self, formatted: bool = True) -> pd.Series:
"""
df = super().summary(formatted=formatted)
assets_number = self.n_assets
effective_nb_assets = self.effective_number_assets
if formatted:
assets_number = str(self.n_assets)
df["Assets number"] = assets_number
assets_number = str(assets_number)
effective_nb_assets = str(effective_nb_assets)
df["Effective Number of Assets"] = effective_nb_assets
df["Assets Number"] = assets_number
return df

def get_weight(self, asset: str) -> float:
Expand Down
30 changes: 24 additions & 6 deletions tests/test_portfolio/test_portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from copy import copy

import numpy as np
import numpy.typing as npt
import pandas as pd
import pytest
import skfolio.measures as mt
Expand All @@ -24,15 +25,15 @@


@pytest.fixture(scope="module")
def X():
def X() -> pd.DataFrame:
prices = load_sp500_dataset()
prices = prices.loc[dt.date(2017, 1, 1) :]
X = prices_to_returns(X=prices)
return X


@pytest.fixture(scope="module")
def weights():
def weights() -> np.ndarray:
weights = np.array([
0.12968013,
0.09150399,
Expand All @@ -58,6 +59,12 @@ def weights():
return weights


@pytest.fixture
def portfolio(X: pd.DataFrame, weights: np.ndarray) -> Portfolio:
portfolio = Portfolio(X=X, weights=weights, annualized_factor=252)
return portfolio


@pytest.fixture(
scope="module",
params=list(PerfMeasure)
Expand Down Expand Up @@ -88,8 +95,7 @@ def _portfolio_returns(asset_returns: np.ndarray, weights: np.array) -> np.array
return returns


def test_pickle(X, weights):
portfolio = Portfolio(X=X, weights=weights, annualized_factor=252)
def test_pickle(portfolio):
portfolio.sharpe_ratio = 5
pickled = pickle.dumps(portfolio)
unpickled = pickle.loads(pickled)
Expand Down Expand Up @@ -298,15 +304,27 @@ def test_portfolio_risk_contribution(X, weights):
assert contribution.shape == (X.shape[1],)


def test_portfolio_metrics(X, weights, measure):
portfolio = Portfolio(X=X, weights=weights, annualized_factor=252)
def test_portfolio_metrics(portfolio, measure):
m = getattr(portfolio, measure.value)
assert isinstance(m, float)
assert not np.isnan(m)
assert portfolio.sric
assert portfolio.skew
assert portfolio.kurtosis
assert portfolio.diversification
assert portfolio.effective_number_assets


def test_portfolio_effective_number_assets(portfolio):
np.testing.assert_almost_equal(portfolio.effective_number_assets, 6.00342169912319)


def test_portfolio_sric(portfolio):
np.testing.assert_almost_equal(portfolio.sric, -0.20309958369097764)


def test_portfolio_diversification(portfolio):
np.testing.assert_almost_equal(portfolio.diversification, 1.449839842913199)


def test_portfolio_slots(X, weights):
Expand Down

0 comments on commit f4dbc5f

Please sign in to comment.