├── .github
└── workflows
│ └── ci.yaml
├── .gitignore
├── LICENSE
├── README.md
├── equity_risk_model
├── __init__.py
├── concentration.py
├── correlation.py
├── model.py
├── optimiser.py
├── risk.py
└── tearsheet.py
├── notebooks
├── 01 - Elementary Risk Models.ipynb
├── 02 - Multi Factor Model.ipynb
└── 03 - Sector Model.ipynb
├── setup.py
└── tests
├── __init__.py
├── conftest.py
├── test_concentration.py
├── test_correlation.py
├── test_optimiser.py
├── test_risk.py
└── test_tearsheet.py
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: Run Python Tests
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 | - name: Install Python 3
16 | uses: actions/setup-python@v1
17 | with:
18 | python-version: '3.x'
19 | - name: Install dependencies
20 | run: |
21 | python -m pip install --upgrade pip
22 | pip install .
23 | pip install pytest
24 | pip install pytest-cov
25 | pip install pytest-cases
26 | - name: Run tests with pytest
27 | run: |
28 | python -m pytest tests/ --doctest-modules --junitxml=junit/test-results.xml --cov=equity_risk_model --cov-report=xml --cov-report=html
29 | coverage report -m
30 | - name: Run coverage
31 | run: |
32 | coverage report -m
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 JM
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # equity-risk-model
2 |
3 | Risk attribution and optimisation using a multi-factor equity risk model.
4 |
5 | Includes functionality to:
6 | * Calculate risk, risk contributions and risk concentration measures
7 | * Optimise a portfolio to be factor *neutral* or *tolerant*
8 |
9 | The directory `notebooks` contains some jupyter notebooks which provide a
10 | starting point to exploring and developing equity risk models. It is by no
11 | means comprehensive, so the usual caveats apply.
12 |
--------------------------------------------------------------------------------
/equity_risk_model/__init__.py:
--------------------------------------------------------------------------------
1 | from . import concentration, correlation, model, optimiser, risk, tearsheet
2 |
--------------------------------------------------------------------------------
/equity_risk_model/concentration.py:
--------------------------------------------------------------------------------
1 | import numpy
2 | import pandas
3 | from typing import Union, Dict
4 |
5 | PortfolioWeights = pandas.Series
6 |
7 |
8 | class ConcentrationCalculator:
9 | """ConcentrationCalculator provides methods to calculate measures of
10 | concentration or diviersification of a portfolio.
11 |
12 | References
13 | ----------
14 | .. Carli, T., Deguest, R. and Martellini, L., 2014. Improved risk reporting
15 | with factor-based diversification measures. EDHEC-Risk Institute
16 | Publications.
17 | """
18 |
19 | def __init__(self, risk_calculator):
20 | self.risk_calculator = risk_calculator
21 |
22 | @staticmethod
23 | def entropy(weights: numpy.array) -> float:
24 | """Entropy of the distribution of portfolio weights
25 |
26 | The ENC measure converges to the entropy of the distribution of
27 | portfolio weights as alpha converges to one.
28 |
29 | Parameters
30 | ----------
31 | weights : numpy.array
32 | Percentage weights for a given portfolio
33 |
34 | Returns
35 | -------
36 | float
37 | The entropy of portfolio weights
38 | """
39 | return numpy.exp(-weights @ numpy.log(weights))
40 |
41 | @staticmethod
42 | def enc(weights: numpy.array, alpha: int = 2) -> float:
43 | """Effective number of constituents
44 |
45 | This function returns the effective number of constituents (ENC) in a
46 | portfolio. Portfolio diversification (respectively, concentration) is
47 | increasing (respectively, decreasing) in the ENC measure. The measure
48 | is directly proportional to the inverse of the variance of the
49 | portfolio weights.
50 |
51 | Taking alpha equal to 2 leads to a diversification measure defined as
52 | the inverse of the Herfindahl-Hirschman index of the percentage weights
53 | in a given portfolio.
54 |
55 | Parameters
56 | ----------
57 | weights : numpy.array
58 | Percentage weights for a given portfolio
59 | alpha : int, optional
60 | A free parameter of the measure, by default 2
61 |
62 | Returns
63 | -------
64 | float
65 | The effective number of constituents in the portfolio
66 | """
67 | return numpy.linalg.norm(weights, alpha) ** (alpha / (1 - alpha))
68 |
69 | def number_of_correlated_bets(self, weights: PortfolioWeights) -> float:
70 | """Effective number of correlated bets in the portfolio
71 |
72 | The normalised contribution to the total risk of the portfolio from
73 | each asset is used in the calculation of the effective number of
74 | constituents.
75 |
76 | Parameters
77 | ----------
78 | weights : PortfolioWeights
79 | The holding weights of each asset of the portfolio
80 |
81 | Returns
82 | -------
83 | float
84 | The effective number of correlated bets in the portfolio
85 |
86 | See Also
87 | --------
88 | equity_risk_model.concentration.enc
89 | """
90 | q = self.risk_calculator.contribution_to_total_risk(
91 | weights
92 | ) / self.risk_calculator.total_risk(weights)
93 |
94 | return self.enc(q)
95 |
96 | def number_of_uncorrelated_bets(self, weights: PortfolioWeights) -> float:
97 | """Effective number of uncorrelated bets in the portfolio
98 |
99 | The normalised contribution to the total specific risk of the portfolio
100 | from each asset is used in the calculation of the effective number
101 | of constituents.
102 |
103 | Parameters
104 | ----------
105 | weights : PortfolioWeights
106 | The holding weights of each asset of the portfolio
107 |
108 | Returns
109 | -------
110 | float
111 | The effective number of uncorrelated bets in the portfolio
112 |
113 | See Also
114 | --------
115 | equity_risk_model.concentration.enc
116 | """
117 | q = self.risk_calculator.contribution_to_total_specific_risk(
118 | weights
119 | ) / self.risk_calculator.total_specific_risk(weights)
120 |
121 | return self.enc(q)
122 |
123 | def min_assets_for_mcsr_threshold(
124 | self, weights: PortfolioWeights, threshold: float = 0.5
125 | ) -> int:
126 | """The minimum number of assets required to reach a given specific risk
127 | contribution threshold
128 |
129 | Parameters
130 | ----------
131 | weights : PortfolioWeights
132 | The holding weights of each asset of the portfolio
133 | threshold : float, optional
134 | The target cumulative specific risk contribution, by default 0.5
135 |
136 | Returns
137 | -------
138 | int
139 | The minimum number of assets required to reach the specific
140 | risk contribution threshold
141 | """
142 |
143 | q = self.risk_calculator.contribution_to_total_specific_risk(
144 | weights
145 | ) / self.risk_calculator.total_specific_risk(weights)
146 |
147 | return (
148 | q.sort_values(ascending=False)
149 | .cumsum()
150 | .loc[lambda x: x <= threshold]
151 | .shape[0]
152 | ) + 1
153 |
154 | def summarise_portfolio(
155 | self, weights: PortfolioWeights
156 | ) -> Dict[str, Union[int, float]]:
157 | """Summarise concentration metrics in dictionary format
158 |
159 | Parameters
160 | ----------
161 | weights : PortfolioWeights
162 | The holding weights of each asset of the portfolio
163 |
164 | Returns
165 | -------
166 | Dict[str, Union[int, float]]
167 | A dictionary summarising a portfolio in terms of concentration
168 | measures
169 | """
170 |
171 | return {
172 | "NAssets": min(len(weights), len(weights[weights != 0])),
173 | "NCorrelatedBets": self.number_of_correlated_bets(weights),
174 | "NUncorrelatedBets": self.number_of_uncorrelated_bets(weights),
175 | "NEffectiveConstituents": self.enc(weights),
176 | "NAssets>25%SpecificRisk": self.min_assets_for_mcsr_threshold(
177 | weights, 0.25
178 | ),
179 | "NAssets>50%SpecificRisk": self.min_assets_for_mcsr_threshold(
180 | weights, 0.5
181 | ),
182 | "NAssets>75%SpecificRisk": self.min_assets_for_mcsr_threshold(
183 | weights, 0.75
184 | ),
185 | }
186 |
--------------------------------------------------------------------------------
/equity_risk_model/correlation.py:
--------------------------------------------------------------------------------
1 | import numpy
2 |
3 |
4 | def is_positive_semidefinite(matrix: numpy.array) -> bool:
5 | """Check whether a matrix is positive semi-definite or not
6 |
7 | Attempt to compute the Cholesky decomposition of the matrix, if this fails
8 | then the matrix is not positive semidefinite.
9 |
10 | Parameters
11 | ----------
12 | matrix : numpy.array
13 | A matrix
14 |
15 | Returns
16 | -------
17 | bool
18 | True if the matrix is positive semidefinite, else False
19 |
20 | References
21 | ----------
22 | .. https://stackoverflow.com/questions/16266720
23 | """
24 | try:
25 | numpy.linalg.cholesky(matrix)
26 | return True
27 | except numpy.linalg.LinAlgError:
28 | return False
29 |
--------------------------------------------------------------------------------
/equity_risk_model/model.py:
--------------------------------------------------------------------------------
1 | import dataclasses
2 | from typing import Dict, List, Union
3 |
4 | import numpy
5 | import pandas
6 |
7 |
8 | @dataclasses.dataclass
9 | class FactorRiskModel:
10 | """Factor Risk Model
11 |
12 | A factor risk model performs factor risk decomposition and single stock
13 | risk attribution. For a universe of n assets and m factors, the risk model
14 | is given by a m by n matrix of factor loadings, a m by m matrix of factor
15 | covariances and a n by n (diagonal) matrix of specific asset returns.
16 |
17 | Attributes
18 | ----------
19 | universe : numpy.array
20 | An array of security identifiers that define the model universe
21 | factors : numpy.array
22 | An array of factor names for factors in the model
23 | loadings : pandas.DataFrame
24 | A matrix of factor loadings for the assets in the universe, the index
25 | should correspond to `factors` whilst the columns should correspond to
26 | `universe`
27 | covariance_factor : pandas.DataFrame
28 | A matrix of factor covariances, both the index and columns should
29 | correspond to `factors`
30 | covariance_specific : pandas.DataFrame
31 | A diagonal matrix of specific variances, both the index and columns
32 | should correspond to `universe`.
33 | factor_group_mapping : Dict[str, str], optional
34 | A dictionary keyed by factor group names with values corresponding to
35 | a list of factor names in the corresponding factor group.
36 | """
37 |
38 | universe: numpy.array
39 | factors: numpy.array
40 | loadings: pandas.DataFrame
41 | covariance_factor: pandas.DataFrame
42 | covariance_specific: pandas.DataFrame
43 | factor_group_mapping: Dict[str, List[str]] = dataclasses.field(
44 | default_factory=lambda: {}
45 | )
46 |
47 | @property
48 | def n_assets(self) -> int:
49 | """Number of assets in the factor model
50 |
51 | Returns
52 | -------
53 | int
54 | The number of assets in the factor model
55 | """
56 | return len(self.universe)
57 |
58 | @property
59 | def n_factors(self) -> int:
60 | """Number of factors in the factor model
61 |
62 | Returns
63 | -------
64 | int
65 | The number of factors in the factor model
66 | """
67 | return len(self.factors)
68 |
69 | @property
70 | def factor_groups(self) -> List[str]:
71 | """Factor groups in the factor model
72 |
73 | Returns
74 | -------
75 | List[str]
76 | A list of factor groups in the factor model (if no factor groups
77 | have been specified then returns an empty list)
78 | """
79 | return list(self.factor_group_mapping.keys())
80 |
81 | @property
82 | def factor_index(self) -> Union[pandas.MultiIndex, pandas.Index]:
83 | """Returns index for factors
84 |
85 | Returns
86 | -------
87 | Union[pandas.MultiIndex, pandas.Index]
88 | An index that can be used for factor ouputs, if a factor group
89 | mapping is provided, then the index will be a multilevel index.
90 | """
91 |
92 | if self.factor_group_mapping:
93 |
94 | idx = []
95 |
96 | for factor in self.factors:
97 |
98 | group = None
99 |
100 | for factor_group, factors in self.factor_group_mapping.items():
101 | if factor in factors:
102 | group = factor_group
103 |
104 | idx.append((group, factor))
105 |
106 | return pandas.MultiIndex.from_tuples(
107 | idx, names=["FactorGroup", "Factor"]
108 | )
109 |
110 | return pandas.Index(self.factors, name="Factor")
111 |
112 | @property
113 | def covariance_total(self) -> numpy.array:
114 | """The covariance matrix for total returns
115 |
116 | Returns
117 | -------
118 | numpy.array
119 | Covariance matrix for total returns
120 | """
121 | return (
122 | self.loadings.T @ self.covariance_factor @ self.loadings
123 | + self.covariance_specific
124 | )
125 |
--------------------------------------------------------------------------------
/equity_risk_model/optimiser.py:
--------------------------------------------------------------------------------
1 | from typing import List, Union
2 |
3 | import cvxpy
4 | import numpy
5 | from cvxpy.constraints.constraint import Constraint
6 | from cvxpy.problems.objective import Maximize, Minimize
7 |
8 | from equity_risk_model.model import FactorRiskModel
9 |
10 |
11 | class PortfolioOptimiser(cvxpy.Problem):
12 | """Wrapper for cvxpy.Problem class to facilitate problem formulation.
13 |
14 | The problem is specified in terms of a quadratic objective function with
15 | affine equality and inequality constraints.
16 |
17 | The standard form is the following:
18 |
19 | .. math::
20 |
21 | \begin{array}{ll}
22 | \mbox{minimize} & (1/2)x^TPx + q^Tx\\
23 | \mbox{subject to} & Gx \leq h \\
24 | & Ax = b.
25 | \end{array}
26 |
27 | The matrices P, G, and A as well as vectors q, h, and b are to be specified
28 | whereas the variable x is the optimisation variable.
29 |
30 | The package `cvxpy` is used to solve the problem.
31 |
32 | See Also
33 | --------
34 | .. https://www.cvxpy.org/index.html
35 | """
36 |
37 | def __init__(self, factor_model: FactorRiskModel):
38 | self.factor_model = factor_model
39 | self.x = cvxpy.Variable(self.factor_model.n_assets)
40 | super().__init__(
41 | objective=self._objective_function(),
42 | constraints=self._constraints(),
43 | )
44 |
45 | def _objective_function(self) -> Union[Minimize, Maximize]:
46 | """Define objective function of the optimisation problem"""
47 | raise NotImplementedError
48 |
49 | def _constraints(self) -> List[Constraint]:
50 | """Define constraints of the optimisation problem"""
51 | raise NotImplementedError
52 |
53 |
54 | class MinimumVariance(PortfolioOptimiser):
55 | """Minimum Variance Long-Only Portfolio
56 |
57 | Find the portfolio weights which minimise the portfolio's variance subject
58 | to positive weights that sum to unity.
59 | """
60 |
61 | def __init__(self, factor_model: FactorRiskModel):
62 | super().__init__(factor_model)
63 |
64 | def _objective_function(self) -> Minimize:
65 | """Objective function for minimum variance optimisation
66 |
67 | Returns
68 | -------
69 | Minimize
70 | Quadratic form to calculate total covariance
71 | """
72 | return cvxpy.Minimize(
73 | # Total Variance
74 | 0.5
75 | * cvxpy.QuadForm(self.x, self.factor_model.covariance_total)
76 | )
77 |
78 | def _constraints(self) -> List[Constraint]:
79 | """Constraints for minimum variance optimisation
80 |
81 | Returns
82 | -------
83 | List[Constraint]
84 | Constraints to impose sum of weights equals unity and that all
85 | weights are positive
86 | """
87 | return [
88 | # Sum of weights equals unity
89 | numpy.ones((self.factor_model.n_assets)).T @ self.x == 1,
90 | # All weights are positive (long only positions)
91 | self.x >= 0,
92 | ]
93 |
94 |
95 | class MaximumSharpe(PortfolioOptimiser):
96 | """Maximum Sharpe Portfolio
97 |
98 | Find the portfolio weights which maximise the portfolio's Sharpe ratio
99 | subject to positive weights that sum to unity.
100 | """
101 |
102 | def __init__(
103 | self,
104 | factor_model: FactorRiskModel,
105 | expected_returns: numpy.ndarray,
106 | gamma: float = 1.0,
107 | ):
108 | self.gamma = gamma
109 | self.expected_returns = expected_returns
110 | super().__init__(factor_model)
111 |
112 | def _objective_function(self) -> Minimize:
113 | """Objective function for maximum Sharpe optimisation
114 |
115 | Returns
116 | -------
117 | Minimize
118 | Quadratic form for total covariance minus risk preference adjusted
119 | expected return
120 | """
121 | return cvxpy.Minimize(
122 | # Total Variance
123 | 0.5 * cvxpy.QuadForm(self.x, self.factor_model.covariance_total)
124 | - self.gamma * self.expected_returns @ self.x
125 | )
126 |
127 | def _constraints(self) -> List[Constraint]:
128 | """Constraints for maximum Sharpe optimisation
129 |
130 | Returns
131 | -------
132 | List[Constraint]
133 | Constraints to impose sum of weights equals unity and that all
134 | weights are positive
135 | """
136 | return [
137 | # Sum of weights equals unity
138 | numpy.ones((self.factor_model.n_assets)).T @ self.x == 1,
139 | # All weights are positive (long only positions)
140 | self.x >= 0,
141 | ]
142 |
143 |
144 | class ProportionalFactorNeutral(PortfolioOptimiser):
145 | """Proportional Factor Neutral Portfolio
146 |
147 | Find the portfolio weights proportional to the expected returns of the
148 | assets subject to the portfolio being factor neutral.
149 | """
150 |
151 | def __init__(
152 | self, factor_model: FactorRiskModel, expected_returns: numpy.ndarray
153 | ):
154 | self.expected_returns = expected_returns
155 | super().__init__(factor_model)
156 |
157 | def _objective_function(self) -> Minimize:
158 | """Objective function for proportional factor neutral optimisation
159 |
160 | Returns
161 | -------
162 | Minimize
163 | Sum of squares distance between weights and expected return
164 | """
165 | return cvxpy.Minimize(
166 | # Distance between expected returns and portfolio weights
167 | cvxpy.sum_squares((self.x - self.expected_returns))
168 | )
169 |
170 | def _constraints(self) -> List[Constraint]:
171 | """Constraints for proportional factor neutral optimisation
172 |
173 | Returns
174 | -------
175 | List[Constraint]
176 | Constraint to impose factor loading of end portfolio is zero
177 | """
178 | return [
179 | # Factor loadings of the portfolio are zero
180 | self.factor_model.loadings.values @ self.x
181 | == numpy.zeros((self.factor_model.n_factors))
182 | ]
183 |
184 |
185 | class InternallyHedgedFactorNeutral(PortfolioOptimiser):
186 | """Internally Hedged Factor Neutral Portfolio
187 |
188 | Finds the (internal) hedge portfolio that results in a factor neutral
189 | portfolio without changing the sign of any weight.
190 | """
191 |
192 | def __init__(
193 | self, factor_model: FactorRiskModel, initial_weights: numpy.ndarray
194 | ):
195 | self.initial_weights = initial_weights
196 | super().__init__(factor_model)
197 |
198 | def _objective_function(self) -> Minimize:
199 | """Objective function for internally hedged factor neutral optimisation
200 |
201 | Returns
202 | -------
203 | Minimize
204 | Quadratic form for specific variance of hedge portfolio
205 | """
206 | return cvxpy.Minimize(
207 | # Specific Variance
208 | 0.5
209 | * cvxpy.QuadForm(self.x, self.factor_model.covariance_specific)
210 | )
211 |
212 | def _constraints(self) -> List[Constraint]:
213 | """Constraints for internally hedged factor neutral optimisation
214 |
215 | Returns
216 | -------
217 | List[Constraint]
218 | Constraint to impose factor loading of end portfolio is zero
219 | """
220 | P = numpy.diag(numpy.sign(self.initial_weights))
221 |
222 | return [
223 | # Factor loadings of the portfolio are zero
224 | self.factor_model.loadings.values @ (self.x + self.initial_weights)
225 | == numpy.zeros((self.factor_model.n_factors)),
226 | # Sign of weights of the portfolio do not change
227 | -P @ self.x <= numpy.abs(self.initial_weights),
228 | ]
229 |
230 |
231 | class InternallyHedgedFactorTolerant(PortfolioOptimiser):
232 | """Internally Hedged Factor Tolerant Portfolio
233 |
234 | Finds the internal hedge portfolio that results in a factor tolerant
235 | portfolio (factor risk within specified bounds).
236 | """
237 |
238 | def __init__(
239 | self,
240 | factor_model: FactorRiskModel,
241 | initial_weights: numpy.ndarray,
242 | factor_risk_upper_bounds: numpy.ndarray,
243 | ):
244 | self.initial_weights = initial_weights
245 | self.factor_risk_upper_bounds = factor_risk_upper_bounds
246 | super().__init__(factor_model)
247 |
248 | def _objective_function(self) -> Minimize:
249 | """Objective function for internally hedged factor tolerant
250 | optimisation
251 |
252 | Returns
253 | -------
254 | Minimize
255 | Quadratic form for specific variance of hedge portfolio plus
256 | quadratic form for factor variance of the end portfolio
257 | """
258 |
259 | cov_factor = (
260 | self.factor_model.loadings.T
261 | @ self.factor_model.covariance_factor
262 | @ self.factor_model.loadings
263 | )
264 |
265 | return cvxpy.Minimize(
266 | # Specific variance of the hedge portfolio
267 | cvxpy.QuadForm(self.x, self.factor_model.covariance_specific)
268 | # Factor variance of the end portfolio
269 | + cvxpy.QuadForm(self.x + self.initial_weights, cov_factor)
270 | )
271 |
272 | def _constraints(self) -> List[Constraint]:
273 | """Constraints for internally hedged factor tolerant optimisation
274 |
275 | Returns
276 | -------
277 | List[Constraint]
278 | Constraint to impose factor risk of each factor is less than
279 | specified upper bound
280 | """
281 |
282 | A = numpy.multiply(
283 | self.factor_model.loadings.T,
284 | numpy.sqrt(numpy.diag(self.factor_model.covariance_factor)),
285 | ).T.values
286 |
287 | return [
288 | # Final portfolio factor risk is less than or equal to upper bound
289 | A @ (self.x + self.initial_weights)
290 | <= self.factor_risk_upper_bounds,
291 | A @ -(self.x + self.initial_weights)
292 | <= self.factor_risk_upper_bounds,
293 | ]
294 |
--------------------------------------------------------------------------------
/equity_risk_model/risk.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import numpy
3 | import pandas
4 | from typing import Dict, Callable
5 |
6 | from .model import FactorRiskModel
7 |
8 |
9 | PortfolioWeights = pandas.Series
10 |
11 |
12 | class RiskCalculator:
13 | """Risk Calculator provides methods to calculate the risk of a portfolio
14 | using and factor risk model.
15 |
16 | References
17 | ----------
18 | .. Menchero, J., Orr, D.J., Wang, J., 2011. The Barra US Equity Model
19 | (USE4) Methodology Notes.
20 | """
21 |
22 | def __init__(self, factor_model: FactorRiskModel):
23 | self.logger = logging.getLogger(self.__class__.__name__)
24 | self.factor_model = factor_model
25 |
26 | def _reindex_weights(func: Callable) -> Callable:
27 | """Define decorator to reindex weights to model universe"""
28 |
29 | def reindex(self, weights: PortfolioWeights) -> Callable:
30 | w = weights.reindex(self.factor_model.universe, axis=0).fillna(
31 | value=0
32 | )
33 |
34 | missing_tickers = set(weights.index).difference(
35 | set(self.factor_model.universe)
36 | )
37 |
38 | if missing_tickers:
39 | self.logger.warning(
40 | "The following tickers are not in the model universe:\n"
41 | f"{missing_tickers}"
42 | )
43 |
44 | return func(self, w)
45 |
46 | return reindex
47 |
48 | @_reindex_weights
49 | def total_risk(self, weights: PortfolioWeights) -> float:
50 | """The total risk of the portfolio
51 |
52 | Parameters
53 | ----------
54 | weights : numpy.array
55 | Asset holding weights of the portfolio
56 |
57 | Returns
58 | -------
59 | float
60 | The total risk of the portfolio
61 | """
62 | return numpy.sqrt(
63 | weights.T @ self.factor_model.covariance_total @ weights
64 | )
65 |
66 | @_reindex_weights
67 | def total_factor_risk(self, weights: PortfolioWeights) -> float:
68 | """The total factor risk of the portfolio
69 |
70 | Parameters
71 | ----------
72 | weights : numpy.array
73 | Asset holding weights of the portfolio
74 |
75 | Returns
76 | -------
77 | float
78 | The total factor risk of the portfolio
79 | """
80 |
81 | x = weights @ self.factor_model.loadings.T
82 | return numpy.sqrt(x.T @ self.factor_model.covariance_factor @ x)
83 |
84 | @_reindex_weights
85 | def total_specific_risk(self, weights: PortfolioWeights) -> float:
86 | """The total specific risk of the portfolio
87 |
88 | Parameters
89 | ----------
90 | weights : numpy.array
91 | The holding weights of each asset of the portfolio
92 |
93 | Returns
94 | -------
95 | float
96 | The total specific risk of the portfolio
97 | """
98 |
99 | return numpy.sqrt(
100 | weights.T @ self.factor_model.covariance_specific @ weights
101 | )
102 |
103 | @_reindex_weights
104 | def factor_group_risks(
105 | self, weights: PortfolioWeights
106 | ) -> Dict[str, float]:
107 | """The risk associated with each factor group in the equity factor
108 | model
109 |
110 | Parameters
111 | ----------
112 | weights : numpy.array
113 | The holding weights of each asset of the portfolio
114 |
115 | Returns
116 | -------
117 | numpy.array
118 | The risk associated with each factor group in the equity factor
119 | model
120 | """
121 | out = {}
122 |
123 | for group, factors in self.factor_model.factor_group_mapping.items():
124 |
125 | loading = self.factor_model.loadings.loc[factors]
126 | cov_f = self.factor_model.covariance_factor.loc[factors, factors]
127 |
128 | sigma_f = loading.T @ cov_f @ loading
129 |
130 | out[group] = numpy.sqrt(weights.T @ sigma_f @ weights)
131 |
132 | return out
133 |
134 | @_reindex_weights
135 | def factor_group_covariance(self, weights: PortfolioWeights) -> float:
136 | """Risk associated with covariances between the factor groups
137 |
138 | Parameters
139 | ----------
140 | weights : numpy.array
141 | The holding weights of each asset of the portfolio
142 |
143 | Returns
144 | -------
145 | float
146 | The risk due to covariances between factors
147 | """
148 | difference = self.total_factor_risk(weights) ** 2 - numpy.sum(
149 | numpy.power(list(self.factor_group_risks(weights).values()), 2)
150 | )
151 |
152 | return numpy.sign(difference) * numpy.sqrt(abs(difference))
153 |
154 | @_reindex_weights
155 | def factor_risks(self, weights: PortfolioWeights) -> numpy.array:
156 | """The risk associated with each factor in the equity factor model
157 |
158 | Parameters
159 | ----------
160 | weights : numpy.array
161 | The holding weights of each asset of the portfolio
162 |
163 | Returns
164 | -------
165 | numpy.array
166 | The risk associated with each factor in the equity factor model
167 | """
168 | factor_risks = numpy.multiply(
169 | # Sign of loading to denote direction
170 | numpy.sign(self.factor_model.loadings @ weights),
171 | # Risk associated with each factor
172 | numpy.sqrt(
173 | numpy.multiply(
174 | (self.factor_model.loadings @ weights) ** 2,
175 | numpy.diag(self.factor_model.covariance_factor),
176 | )
177 | ),
178 | )
179 |
180 | factor_risks.index = self.factor_model.factor_index
181 |
182 | return factor_risks
183 |
184 | @_reindex_weights
185 | def factor_risk_covariance(self, weights: PortfolioWeights) -> float:
186 | """Risk associated with covariances between the factors
187 |
188 | Parameters
189 | ----------
190 | weights : numpy.array
191 | The holding weights of each asset of the portfolio
192 |
193 | Returns
194 | -------
195 | float
196 | The risk due to covariances between factors
197 | """
198 | difference = self.total_factor_risk(weights) ** 2 - numpy.sum(
199 | self.factor_risks(weights) ** 2
200 | )
201 |
202 | return numpy.sign(difference) * numpy.sqrt(abs(difference))
203 |
204 | @_reindex_weights
205 | def contribution_to_total_risk(
206 | self, weights: PortfolioWeights
207 | ) -> numpy.array:
208 | """Contribution to the total risk from each asset
209 |
210 | Parameters
211 | ----------
212 | weights : numpy.array
213 | The holding weights of each asset of the portfolio
214 |
215 | Returns
216 | -------
217 | numpy.array
218 | An array of risk contributions to the total risk from each asset
219 | in the portfolio
220 | """
221 | return numpy.multiply(
222 | weights, self.factor_model.covariance_total @ weights
223 | ) / self.total_risk(weights)
224 |
225 | @_reindex_weights
226 | def contribution_to_total_factor_risk(
227 | self, weights: numpy.array
228 | ) -> numpy.array:
229 | """Contribution to the total factor risk from each asset
230 |
231 | Parameters
232 | ----------
233 | weights : numpy.array
234 | The holding weights of each asset of the portfolio
235 |
236 | Returns
237 | -------
238 | numpy.array
239 | An array of risk contributions to the total factor risk from each
240 | asset in the portfolio
241 | """
242 | cov = (
243 | self.factor_model.loadings.T
244 | @ self.factor_model.covariance_factor
245 | @ self.factor_model.loadings
246 | )
247 |
248 | return numpy.multiply(weights, cov @ weights) / self.total_factor_risk(
249 | weights
250 | )
251 |
252 | @_reindex_weights
253 | def contribution_to_total_specific_risk(
254 | self, weights: PortfolioWeights
255 | ) -> numpy.array:
256 | """Contribution to the total specific risk from each asset
257 |
258 | Parameters
259 | ----------
260 | weights : numpy.array
261 | The holding weights of each asset of the portfolio
262 |
263 | Returns
264 | -------
265 | numpy.array
266 | An array of risk contributions to the total specific risk from
267 | each asset in the portfolio
268 | """
269 | return numpy.multiply(
270 | weights, self.factor_model.covariance_specific @ weights
271 | ) / self.total_specific_risk(weights)
272 |
273 | @_reindex_weights
274 | def contributions_to_factor_risks(
275 | self, weights: PortfolioWeights
276 | ) -> pandas.DataFrame:
277 | """Contributions to the risk of each factor from each asset
278 |
279 | Parameters
280 | ----------
281 | weights : numpy.array
282 | The holding weights of each asset of the portfolio
283 |
284 | Returns
285 | -------
286 | pandas.DataFrame
287 | A matrix of risk contributions where element [i, j] is the
288 | contribution of asset i to factor j.
289 | """
290 | # Only take diagonal elements
291 | cov = numpy.diag(numpy.diag(self.factor_model.covariance_factor))
292 |
293 | return numpy.divide(
294 | numpy.multiply(
295 | numpy.multiply(self.factor_model.loadings, weights.T).T,
296 | (cov @ self.factor_model.loadings @ weights),
297 | ),
298 | self.factor_risks(weights).values,
299 | ).fillna(value=0)
300 |
--------------------------------------------------------------------------------
/equity_risk_model/tearsheet.py:
--------------------------------------------------------------------------------
1 | import abc
2 | import numpy
3 | import pandas
4 | from typing import Dict, Union
5 |
6 | from equity_risk_model.risk import RiskCalculator
7 | from equity_risk_model.concentration import ConcentrationCalculator
8 |
9 |
10 | PortfolioWeights = Union[numpy.array, pandas.Series]
11 |
12 |
13 | class BaseTearsheet:
14 | """Class to create a DataFrame summary of a portfolio"""
15 |
16 | @abc.abstractmethod
17 | def create_portfolio_panel(
18 | self, weights: PortfolioWeights
19 | ) -> pandas.Series:
20 | """Create a panel summarising a portfolio from portfolio weights
21 |
22 | Parameters
23 | ----------
24 | weights : PortfolioWeights
25 | The holding weights of each asset of the portfolio
26 |
27 | Returns
28 | -------
29 | pandas.Series
30 | A pandas series with summary information of the portfolio
31 |
32 | """
33 | raise NotImplementedError
34 |
35 | def create_tearsheet(
36 | self, portfolio_weights: Dict[str, PortfolioWeights]
37 | ) -> pandas.DataFrame:
38 | """Create a tearsheet from a dictionary of portfolio weights
39 |
40 | Parameters
41 | ----------
42 | portfolio_weights : Dict[str, PortfolioWeights]
43 | A dictionary of portfolio weights, the key is used as the portfolio
44 | identifier in the DataFrame
45 |
46 | Returns
47 | -------
48 | pandas.DataFrame
49 | A DataFrame providing a tabular summary of the portfolios
50 | """
51 | return pandas.DataFrame(
52 | {
53 | portfolio_name: self.create_portfolio_panel(weights)
54 | for portfolio_name, weights in portfolio_weights.items()
55 | }
56 | )
57 |
58 |
59 | class ConcentrationTearsheet(BaseTearsheet):
60 | """Class to provide a summary of concentration metrics for portfolios"""
61 |
62 | def __init__(self, concentration_calculator: ConcentrationCalculator):
63 | self.concentration_calculator = concentration_calculator
64 |
65 | def create_portfolio_panel(
66 | self, weights: PortfolioWeights
67 | ) -> pandas.Series:
68 | return pandas.Series(
69 | self.concentration_calculator.summarise_portfolio(weights)
70 | )
71 |
72 |
73 | class RiskTearsheet(BaseTearsheet):
74 | """Class the provides a summary of a portfolio's factor risk"""
75 |
76 | def __init__(self, risk_calculator: RiskCalculator):
77 | self.risk_calculator = risk_calculator
78 |
79 |
80 | class FactorRiskSummaryTearsheet(RiskTearsheet):
81 | """Tearsheet summarising total, factor and specific risk"""
82 |
83 | def create_portfolio_panel(
84 | self, weights: PortfolioWeights
85 | ) -> pandas.Series:
86 | return pandas.Series(
87 | {
88 | "Total": self.risk_calculator.total_risk(weights),
89 | "Factor": self.risk_calculator.total_factor_risk(weights),
90 | "Specific": self.risk_calculator.total_specific_risk(weights),
91 | }
92 | )
93 |
94 |
95 | class FactorGroupRiskTearsheet(RiskTearsheet):
96 | """Tearsheet summarising factor group risk"""
97 |
98 | def create_portfolio_panel(
99 | self, weights: PortfolioWeights
100 | ) -> pandas.Series:
101 | return pandas.Series(
102 | self.risk_calculator.factor_group_risks(weights)
103 | | {
104 | "Covariance": self.risk_calculator.factor_group_covariance(
105 | weights
106 | )
107 | }
108 | )
109 |
110 |
111 | class FactorRiskTearsheet(RiskTearsheet):
112 | """Tearsheet summarising individual factor risks"""
113 |
114 | def create_portfolio_panel(
115 | self, weights: PortfolioWeights
116 | ) -> pandas.Series:
117 | return self.risk_calculator.factor_risks(weights)
118 |
--------------------------------------------------------------------------------
/notebooks/01 - Elementary Risk Models.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "id": "94655dbb-e2f7-4777-a0ab-84867eba5410",
7 | "metadata": {},
8 | "outputs": [],
9 | "source": [
10 | "import pandas\n",
11 | "import numpy\n",
12 | "import yfinance\n",
13 | "import statsmodels.api as sm\n",
14 | "\n",
15 | "import matplotlib.pyplot as plt\n",
16 | "import matplotlib"
17 | ]
18 | },
19 | {
20 | "cell_type": "markdown",
21 | "id": "882669d5-62b4-47b5-a5cd-9dd87ebf28d2",
22 | "metadata": {},
23 | "source": [
24 | "# 1 Introduction\n",
25 | "\n",
26 | "We introduce some elementary risk models to derive the covariance matrix between stocks.\n",
27 | "\n",
28 | "### References\n",
29 | "\n",
30 | "* Grinold, R. C., & Kahn, R. N. (2000). Active portfolio management. (Chapter 3)"
31 | ]
32 | },
33 | {
34 | "cell_type": "markdown",
35 | "id": "899834c6-ba7b-4f61-8446-3b0903a8a767",
36 | "metadata": {},
37 | "source": [
38 | "### Fetch Data"
39 | ]
40 | },
41 | {
42 | "cell_type": "code",
43 | "execution_count": 2,
44 | "id": "38a94246-6b8e-4bd7-9e0b-944b16db7579",
45 | "metadata": {},
46 | "outputs": [],
47 | "source": [
48 | "stocks = [\"ACN\", \"BA\", \"BAC\", \"CAT\", \"HP\", \"INTC\", \"KMB\", \"NVDA\", \"NFLX\", \"XRX\"]\n",
49 | "benchmark = \"SPY\""
50 | ]
51 | },
52 | {
53 | "cell_type": "code",
54 | "execution_count": 3,
55 | "id": "2479d946-09b8-4cd5-80c8-90e5c7a2cc9f",
56 | "metadata": {},
57 | "outputs": [
58 | {
59 | "name": "stdout",
60 | "output_type": "stream",
61 | "text": [
62 | "[*********************100%%**********************] 11 of 11 completed\n"
63 | ]
64 | }
65 | ],
66 | "source": [
67 | "yf = yfinance.Tickers(stocks + [benchmark])\n",
68 | "df = yf.download(\"5y\")"
69 | ]
70 | },
71 | {
72 | "cell_type": "code",
73 | "execution_count": 4,
74 | "id": "bcc5e56d-5aa1-4365-8cc6-09b3d1173410",
75 | "metadata": {},
76 | "outputs": [],
77 | "source": [
78 | "df_returns = df[\"Close\"].pct_change().dropna()"
79 | ]
80 | },
81 | {
82 | "cell_type": "markdown",
83 | "id": "500a9f9e-df0c-4945-924b-c7211b7a9dac",
84 | "metadata": {},
85 | "source": [
86 | "### Active and Residual Risk\n",
87 | "\n",
88 | "The active return of a stock against a benchmark is given by,\n",
89 | "\n",
90 | "$$ r_{active} = r_{stock} - r_{benchmark} $$\n",
91 | "\n",
92 | "and the active risk is given by\n",
93 | "\n",
94 | "$$ \\sigma_{active} = Std(r_{active}) = Std(r_{stock} - r_{benchmark}) $$\n",
95 | "\n",
96 | "The residual risk is the risk orthogonal to the systematic risk, and is given by\n",
97 | "\n",
98 | "$$ \\omega_p = \\sqrt{\\sigma_P^2 - \\beta_P^2 \\sigma_B^2} $$\n",
99 | "\n",
100 | "where\n",
101 | "\n",
102 | "$$ \\beta_P = \\frac{Cov(r_P, r_B)}{Var(r_B)}. $$"
103 | ]
104 | },
105 | {
106 | "cell_type": "markdown",
107 | "id": "9ce93e01-cf5e-43e5-8dc6-8bf429c9dfc5",
108 | "metadata": {},
109 | "source": [
110 | "For each stock, the beta, total risk and residual risk with respect to the benchmark can be calculated."
111 | ]
112 | },
113 | {
114 | "cell_type": "code",
115 | "execution_count": 5,
116 | "id": "dd483627-450d-4669-9fed-9a41b5e67655",
117 | "metadata": {},
118 | "outputs": [],
119 | "source": [
120 | "def calculate_risk_summary(df_returns, stocks, benchmark):\n",
121 | " out = []\n",
122 | " \n",
123 | " for stock in stocks:\n",
124 | " \n",
125 | " exog = sm.add_constant(df_returns[benchmark], prepend=True)\n",
126 | " endog = df_returns[stock]\n",
127 | "\n",
128 | " lr = sm.OLS(endog=endog, exog=exog)\n",
129 | " beta = lr.fit().params.loc[benchmark]\n",
130 | "\n",
131 | " stock_risk = df_returns[stock].std()\n",
132 | " market_risk = df_returns[benchmark].std()\n",
133 | "\n",
134 | " residual_risk = numpy.sqrt(df_returns[stock].std()**2 - (beta * df_returns[benchmark].std()) ** 2)\n",
135 | "\n",
136 | " out.append({\"Stock\": stock, \"Beta\": beta, \"TotalRisk\": stock_risk, \"ResidualRisk\": residual_risk})\n",
137 | "\n",
138 | " return pandas.DataFrame(out).set_index(keys=[\"Stock\"])"
139 | ]
140 | },
141 | {
142 | "cell_type": "code",
143 | "execution_count": 6,
144 | "id": "d4ddcdad-c280-4bc5-8e74-2245f3d98e7c",
145 | "metadata": {},
146 | "outputs": [
147 | {
148 | "data": {
149 | "text/html": [
150 | "
\n",
151 | "\n",
164 | "
\n",
165 | " \n",
166 | " \n",
167 | " | \n",
168 | " Beta | \n",
169 | " TotalRisk | \n",
170 | " ResidualRisk | \n",
171 | "
\n",
172 | " \n",
173 | " Stock | \n",
174 | " | \n",
175 | " | \n",
176 | " | \n",
177 | "
\n",
178 | " \n",
179 | " \n",
180 | " \n",
181 | " ACN | \n",
182 | " 1.105547 | \n",
183 | " 0.017772 | \n",
184 | " 0.010200 | \n",
185 | "
\n",
186 | " \n",
187 | " BA | \n",
188 | " 1.485289 | \n",
189 | " 0.031934 | \n",
190 | " 0.025249 | \n",
191 | "
\n",
192 | " \n",
193 | " BAC | \n",
194 | " 1.241215 | \n",
195 | " 0.022438 | \n",
196 | " 0.015379 | \n",
197 | "
\n",
198 | " \n",
199 | " CAT | \n",
200 | " 0.977179 | \n",
201 | " 0.020312 | \n",
202 | " 0.015720 | \n",
203 | "
\n",
204 | " \n",
205 | " HP | \n",
206 | " 1.499200 | \n",
207 | " 0.038074 | \n",
208 | " 0.032560 | \n",
209 | "
\n",
210 | " \n",
211 | " INTC | \n",
212 | " 1.228634 | \n",
213 | " 0.024580 | \n",
214 | " 0.018509 | \n",
215 | "
\n",
216 | " \n",
217 | " KMB | \n",
218 | " 0.452469 | \n",
219 | " 0.013660 | \n",
220 | " 0.012293 | \n",
221 | "
\n",
222 | " \n",
223 | " NVDA | \n",
224 | " 1.751760 | \n",
225 | " 0.032228 | \n",
226 | " 0.022514 | \n",
227 | "
\n",
228 | " \n",
229 | " NFLX | \n",
230 | " 1.053397 | \n",
231 | " 0.028989 | \n",
232 | " 0.025457 | \n",
233 | "
\n",
234 | " \n",
235 | " XRX | \n",
236 | " 1.293811 | \n",
237 | " 0.028809 | \n",
238 | " 0.023236 | \n",
239 | "
\n",
240 | " \n",
241 | "
\n",
242 | "
"
243 | ],
244 | "text/plain": [
245 | " Beta TotalRisk ResidualRisk\n",
246 | "Stock \n",
247 | "ACN 1.105547 0.017772 0.010200\n",
248 | "BA 1.485289 0.031934 0.025249\n",
249 | "BAC 1.241215 0.022438 0.015379\n",
250 | "CAT 0.977179 0.020312 0.015720\n",
251 | "HP 1.499200 0.038074 0.032560\n",
252 | "INTC 1.228634 0.024580 0.018509\n",
253 | "KMB 0.452469 0.013660 0.012293\n",
254 | "NVDA 1.751760 0.032228 0.022514\n",
255 | "NFLX 1.053397 0.028989 0.025457\n",
256 | "XRX 1.293811 0.028809 0.023236"
257 | ]
258 | },
259 | "execution_count": 6,
260 | "metadata": {},
261 | "output_type": "execute_result"
262 | }
263 | ],
264 | "source": [
265 | "calculate_risk_summary(df_returns, stocks, benchmark)"
266 | ]
267 | },
268 | {
269 | "cell_type": "markdown",
270 | "id": "5a5c920d-cf6c-4efa-9930-f67703a5a0f8",
271 | "metadata": {},
272 | "source": [
273 | "# 2 Risk Models"
274 | ]
275 | },
276 | {
277 | "cell_type": "markdown",
278 | "id": "89b6f928-5b24-4527-a46a-241ef69f2fda",
279 | "metadata": {},
280 | "source": [
281 | "### 2.1 Historical Covariance Matrix\n",
282 | "\n",
283 | "The simplest method is to rely on the historical covariances between the stocks. This approach is not robust when there is a large number of stocks $N$ and a limited time-series ($T$) of data, i.e, when $T < N$, as the covariance matrix becomes singular. Further, even if we had a long enough time-series we may wish to exclude data that is \"old\" and no longer \"relevant\" due to the changing nature of the market."
284 | ]
285 | },
286 | {
287 | "cell_type": "code",
288 | "execution_count": 7,
289 | "id": "a5c01097-6613-494f-98a1-f917e23fc7d2",
290 | "metadata": {},
291 | "outputs": [],
292 | "source": [
293 | "def cov_empirical(df_returns, stocks, benchmarks):\n",
294 | " return df_returns[stocks].cov()"
295 | ]
296 | },
297 | {
298 | "cell_type": "code",
299 | "execution_count": 8,
300 | "id": "2e3efc0f-9447-49a9-8111-abef36643b86",
301 | "metadata": {},
302 | "outputs": [
303 | {
304 | "data": {
305 | "text/html": [
306 | "\n",
307 | "\n",
320 | "
\n",
321 | " \n",
322 | " \n",
323 | " | \n",
324 | " ACN | \n",
325 | " BA | \n",
326 | " BAC | \n",
327 | " CAT | \n",
328 | " HP | \n",
329 | " INTC | \n",
330 | " KMB | \n",
331 | " NVDA | \n",
332 | " NFLX | \n",
333 | " XRX | \n",
334 | "
\n",
335 | " \n",
336 | " \n",
337 | " \n",
338 | " ACN | \n",
339 | " 0.000316 | \n",
340 | " 0.000259 | \n",
341 | " 0.000228 | \n",
342 | " 0.000179 | \n",
343 | " 0.000251 | \n",
344 | " 0.000223 | \n",
345 | " 0.000095 | \n",
346 | " 0.000324 | \n",
347 | " 0.000206 | \n",
348 | " 0.000256 | \n",
349 | "
\n",
350 | " \n",
351 | " BA | \n",
352 | " 0.000259 | \n",
353 | " 0.001020 | \n",
354 | " 0.000409 | \n",
355 | " 0.000320 | \n",
356 | " 0.000583 | \n",
357 | " 0.000314 | \n",
358 | " 0.000071 | \n",
359 | " 0.000367 | \n",
360 | " 0.000214 | \n",
361 | " 0.000472 | \n",
362 | "
\n",
363 | " \n",
364 | " BAC | \n",
365 | " 0.000228 | \n",
366 | " 0.000409 | \n",
367 | " 0.000503 | \n",
368 | " 0.000303 | \n",
369 | " 0.000499 | \n",
370 | " 0.000243 | \n",
371 | " 0.000079 | \n",
372 | " 0.000272 | \n",
373 | " 0.000136 | \n",
374 | " 0.000376 | \n",
375 | "
\n",
376 | " \n",
377 | " CAT | \n",
378 | " 0.000179 | \n",
379 | " 0.000320 | \n",
380 | " 0.000303 | \n",
381 | " 0.000413 | \n",
382 | " 0.000427 | \n",
383 | " 0.000200 | \n",
384 | " 0.000056 | \n",
385 | " 0.000231 | \n",
386 | " 0.000105 | \n",
387 | " 0.000324 | \n",
388 | "
\n",
389 | " \n",
390 | " HP | \n",
391 | " 0.000251 | \n",
392 | " 0.000583 | \n",
393 | " 0.000499 | \n",
394 | " 0.000427 | \n",
395 | " 0.001450 | \n",
396 | " 0.000323 | \n",
397 | " 0.000063 | \n",
398 | " 0.000326 | \n",
399 | " 0.000151 | \n",
400 | " 0.000508 | \n",
401 | "
\n",
402 | " \n",
403 | " INTC | \n",
404 | " 0.000223 | \n",
405 | " 0.000314 | \n",
406 | " 0.000243 | \n",
407 | " 0.000200 | \n",
408 | " 0.000323 | \n",
409 | " 0.000604 | \n",
410 | " 0.000087 | \n",
411 | " 0.000433 | \n",
412 | " 0.000261 | \n",
413 | " 0.000271 | \n",
414 | "
\n",
415 | " \n",
416 | " KMB | \n",
417 | " 0.000095 | \n",
418 | " 0.000071 | \n",
419 | " 0.000079 | \n",
420 | " 0.000056 | \n",
421 | " 0.000063 | \n",
422 | " 0.000087 | \n",
423 | " 0.000187 | \n",
424 | " 0.000075 | \n",
425 | " 0.000056 | \n",
426 | " 0.000073 | \n",
427 | "
\n",
428 | " \n",
429 | " NVDA | \n",
430 | " 0.000324 | \n",
431 | " 0.000367 | \n",
432 | " 0.000272 | \n",
433 | " 0.000231 | \n",
434 | " 0.000326 | \n",
435 | " 0.000433 | \n",
436 | " 0.000075 | \n",
437 | " 0.001039 | \n",
438 | " 0.000447 | \n",
439 | " 0.000310 | \n",
440 | "
\n",
441 | " \n",
442 | " NFLX | \n",
443 | " 0.000206 | \n",
444 | " 0.000214 | \n",
445 | " 0.000136 | \n",
446 | " 0.000105 | \n",
447 | " 0.000151 | \n",
448 | " 0.000261 | \n",
449 | " 0.000056 | \n",
450 | " 0.000447 | \n",
451 | " 0.000840 | \n",
452 | " 0.000194 | \n",
453 | "
\n",
454 | " \n",
455 | " XRX | \n",
456 | " 0.000256 | \n",
457 | " 0.000472 | \n",
458 | " 0.000376 | \n",
459 | " 0.000324 | \n",
460 | " 0.000508 | \n",
461 | " 0.000271 | \n",
462 | " 0.000073 | \n",
463 | " 0.000310 | \n",
464 | " 0.000194 | \n",
465 | " 0.000830 | \n",
466 | "
\n",
467 | " \n",
468 | "
\n",
469 | "
"
470 | ],
471 | "text/plain": [
472 | " ACN BA BAC CAT HP INTC KMB \\\n",
473 | "ACN 0.000316 0.000259 0.000228 0.000179 0.000251 0.000223 0.000095 \n",
474 | "BA 0.000259 0.001020 0.000409 0.000320 0.000583 0.000314 0.000071 \n",
475 | "BAC 0.000228 0.000409 0.000503 0.000303 0.000499 0.000243 0.000079 \n",
476 | "CAT 0.000179 0.000320 0.000303 0.000413 0.000427 0.000200 0.000056 \n",
477 | "HP 0.000251 0.000583 0.000499 0.000427 0.001450 0.000323 0.000063 \n",
478 | "INTC 0.000223 0.000314 0.000243 0.000200 0.000323 0.000604 0.000087 \n",
479 | "KMB 0.000095 0.000071 0.000079 0.000056 0.000063 0.000087 0.000187 \n",
480 | "NVDA 0.000324 0.000367 0.000272 0.000231 0.000326 0.000433 0.000075 \n",
481 | "NFLX 0.000206 0.000214 0.000136 0.000105 0.000151 0.000261 0.000056 \n",
482 | "XRX 0.000256 0.000472 0.000376 0.000324 0.000508 0.000271 0.000073 \n",
483 | "\n",
484 | " NVDA NFLX XRX \n",
485 | "ACN 0.000324 0.000206 0.000256 \n",
486 | "BA 0.000367 0.000214 0.000472 \n",
487 | "BAC 0.000272 0.000136 0.000376 \n",
488 | "CAT 0.000231 0.000105 0.000324 \n",
489 | "HP 0.000326 0.000151 0.000508 \n",
490 | "INTC 0.000433 0.000261 0.000271 \n",
491 | "KMB 0.000075 0.000056 0.000073 \n",
492 | "NVDA 0.001039 0.000447 0.000310 \n",
493 | "NFLX 0.000447 0.000840 0.000194 \n",
494 | "XRX 0.000310 0.000194 0.000830 "
495 | ]
496 | },
497 | "execution_count": 8,
498 | "metadata": {},
499 | "output_type": "execute_result"
500 | }
501 | ],
502 | "source": [
503 | "cov_empirical(df_returns, stocks, benchmark)"
504 | ]
505 | },
506 | {
507 | "cell_type": "markdown",
508 | "id": "68768fe9-d36e-40ba-847a-b89fd857ba27",
509 | "metadata": {},
510 | "source": [
511 | "### 2.2 The Single Factor Risk Model\n",
512 | "\n",
513 | "For the single factor risk model we examine returns in the following manner\n",
514 | "\n",
515 | "$$ r_i = \\beta_i r_M + \\epsilon_i $$\n",
516 | "\n",
517 | "where $r_i$ is the return of stock $i$, $\\beta_i$ is the beta of stock $i$ to the market's return $r_M$, and $\\epsilon_i$ is the residual return.\n",
518 | "\n",
519 | "The model assumes that the residual returns, $\\epsilon_i$ are uncorrelated, therefore the covariance between two stocks is given by\n",
520 | "\n",
521 | "$$ Cov(r_i, r_j) = \\beta_i \\beta_j \\sigma_M^2 $$\n",
522 | "\n",
523 | "and the variance of a stock is given by\n",
524 | "\n",
525 | "$$ \\sigma_i^2 = \\beta_i^2 \\sigma_M^2 + \\omega_i^2 $$\n",
526 | "\n",
527 | "where $\\omega_i$ is the residual risk of stock $n$."
528 | ]
529 | },
530 | {
531 | "cell_type": "code",
532 | "execution_count": 9,
533 | "id": "00ba2ee8-135e-42cc-b6d4-30920cf44734",
534 | "metadata": {},
535 | "outputs": [],
536 | "source": [
537 | "def single_factor_covariance(df_returns, stocks, benchmark):\n",
538 | "\n",
539 | " df_risk_summary = calculate_risk_summary(df_returns, stocks, benchmark)\n",
540 | "\n",
541 | " benchmark_var = df_returns[benchmark].var()\n",
542 | "\n",
543 | " cov_sfr = (\n",
544 | " numpy.outer(df_risk_summary[\"Beta\"], df_risk_summary[\"Beta\"]) * benchmark_var +\n",
545 | " numpy.diag(df_risk_summary[\"ResidualRisk\"] ** 2)\n",
546 | " )\n",
547 | "\n",
548 | " return pandas.DataFrame(\n",
549 | " data=cov_sfr,\n",
550 | " index=df_risk_summary.index,\n",
551 | " columns=df_risk_summary.index\n",
552 | " )"
553 | ]
554 | },
555 | {
556 | "cell_type": "code",
557 | "execution_count": 10,
558 | "id": "be377e6b-e6f3-45af-8b31-57922d941202",
559 | "metadata": {},
560 | "outputs": [
561 | {
562 | "data": {
563 | "text/html": [
564 | "\n",
565 | "\n",
578 | "
\n",
579 | " \n",
580 | " \n",
581 | " Stock | \n",
582 | " ACN | \n",
583 | " BA | \n",
584 | " BAC | \n",
585 | " CAT | \n",
586 | " HP | \n",
587 | " INTC | \n",
588 | " KMB | \n",
589 | " NVDA | \n",
590 | " NFLX | \n",
591 | " XRX | \n",
592 | "
\n",
593 | " \n",
594 | " Stock | \n",
595 | " | \n",
596 | " | \n",
597 | " | \n",
598 | " | \n",
599 | " | \n",
600 | " | \n",
601 | " | \n",
602 | " | \n",
603 | " | \n",
604 | " | \n",
605 | "
\n",
606 | " \n",
607 | " \n",
608 | " \n",
609 | " ACN | \n",
610 | " 0.000316 | \n",
611 | " 0.000285 | \n",
612 | " 0.000238 | \n",
613 | " 0.000187 | \n",
614 | " 0.000287 | \n",
615 | " 0.000235 | \n",
616 | " 0.000087 | \n",
617 | " 0.000336 | \n",
618 | " 0.000202 | \n",
619 | " 0.000248 | \n",
620 | "
\n",
621 | " \n",
622 | " BA | \n",
623 | " 0.000285 | \n",
624 | " 0.001020 | \n",
625 | " 0.000319 | \n",
626 | " 0.000252 | \n",
627 | " 0.000386 | \n",
628 | " 0.000316 | \n",
629 | " 0.000116 | \n",
630 | " 0.000451 | \n",
631 | " 0.000271 | \n",
632 | " 0.000333 | \n",
633 | "
\n",
634 | " \n",
635 | " BAC | \n",
636 | " 0.000238 | \n",
637 | " 0.000319 | \n",
638 | " 0.000503 | \n",
639 | " 0.000210 | \n",
640 | " 0.000322 | \n",
641 | " 0.000264 | \n",
642 | " 0.000097 | \n",
643 | " 0.000377 | \n",
644 | " 0.000227 | \n",
645 | " 0.000278 | \n",
646 | "
\n",
647 | " \n",
648 | " CAT | \n",
649 | " 0.000187 | \n",
650 | " 0.000252 | \n",
651 | " 0.000210 | \n",
652 | " 0.000413 | \n",
653 | " 0.000254 | \n",
654 | " 0.000208 | \n",
655 | " 0.000077 | \n",
656 | " 0.000297 | \n",
657 | " 0.000178 | \n",
658 | " 0.000219 | \n",
659 | "
\n",
660 | " \n",
661 | " HP | \n",
662 | " 0.000287 | \n",
663 | " 0.000386 | \n",
664 | " 0.000322 | \n",
665 | " 0.000254 | \n",
666 | " 0.001450 | \n",
667 | " 0.000319 | \n",
668 | " 0.000118 | \n",
669 | " 0.000455 | \n",
670 | " 0.000274 | \n",
671 | " 0.000336 | \n",
672 | "
\n",
673 | " \n",
674 | " INTC | \n",
675 | " 0.000235 | \n",
676 | " 0.000316 | \n",
677 | " 0.000264 | \n",
678 | " 0.000208 | \n",
679 | " 0.000319 | \n",
680 | " 0.000604 | \n",
681 | " 0.000096 | \n",
682 | " 0.000373 | \n",
683 | " 0.000224 | \n",
684 | " 0.000275 | \n",
685 | "
\n",
686 | " \n",
687 | " KMB | \n",
688 | " 0.000087 | \n",
689 | " 0.000116 | \n",
690 | " 0.000097 | \n",
691 | " 0.000077 | \n",
692 | " 0.000118 | \n",
693 | " 0.000096 | \n",
694 | " 0.000187 | \n",
695 | " 0.000137 | \n",
696 | " 0.000083 | \n",
697 | " 0.000101 | \n",
698 | "
\n",
699 | " \n",
700 | " NVDA | \n",
701 | " 0.000336 | \n",
702 | " 0.000451 | \n",
703 | " 0.000377 | \n",
704 | " 0.000297 | \n",
705 | " 0.000455 | \n",
706 | " 0.000373 | \n",
707 | " 0.000137 | \n",
708 | " 0.001039 | \n",
709 | " 0.000320 | \n",
710 | " 0.000393 | \n",
711 | "
\n",
712 | " \n",
713 | " NFLX | \n",
714 | " 0.000202 | \n",
715 | " 0.000271 | \n",
716 | " 0.000227 | \n",
717 | " 0.000178 | \n",
718 | " 0.000274 | \n",
719 | " 0.000224 | \n",
720 | " 0.000083 | \n",
721 | " 0.000320 | \n",
722 | " 0.000840 | \n",
723 | " 0.000236 | \n",
724 | "
\n",
725 | " \n",
726 | " XRX | \n",
727 | " 0.000248 | \n",
728 | " 0.000333 | \n",
729 | " 0.000278 | \n",
730 | " 0.000219 | \n",
731 | " 0.000336 | \n",
732 | " 0.000275 | \n",
733 | " 0.000101 | \n",
734 | " 0.000393 | \n",
735 | " 0.000236 | \n",
736 | " 0.000830 | \n",
737 | "
\n",
738 | " \n",
739 | "
\n",
740 | "
"
741 | ],
742 | "text/plain": [
743 | "Stock ACN BA BAC CAT HP INTC KMB \\\n",
744 | "Stock \n",
745 | "ACN 0.000316 0.000285 0.000238 0.000187 0.000287 0.000235 0.000087 \n",
746 | "BA 0.000285 0.001020 0.000319 0.000252 0.000386 0.000316 0.000116 \n",
747 | "BAC 0.000238 0.000319 0.000503 0.000210 0.000322 0.000264 0.000097 \n",
748 | "CAT 0.000187 0.000252 0.000210 0.000413 0.000254 0.000208 0.000077 \n",
749 | "HP 0.000287 0.000386 0.000322 0.000254 0.001450 0.000319 0.000118 \n",
750 | "INTC 0.000235 0.000316 0.000264 0.000208 0.000319 0.000604 0.000096 \n",
751 | "KMB 0.000087 0.000116 0.000097 0.000077 0.000118 0.000096 0.000187 \n",
752 | "NVDA 0.000336 0.000451 0.000377 0.000297 0.000455 0.000373 0.000137 \n",
753 | "NFLX 0.000202 0.000271 0.000227 0.000178 0.000274 0.000224 0.000083 \n",
754 | "XRX 0.000248 0.000333 0.000278 0.000219 0.000336 0.000275 0.000101 \n",
755 | "\n",
756 | "Stock NVDA NFLX XRX \n",
757 | "Stock \n",
758 | "ACN 0.000336 0.000202 0.000248 \n",
759 | "BA 0.000451 0.000271 0.000333 \n",
760 | "BAC 0.000377 0.000227 0.000278 \n",
761 | "CAT 0.000297 0.000178 0.000219 \n",
762 | "HP 0.000455 0.000274 0.000336 \n",
763 | "INTC 0.000373 0.000224 0.000275 \n",
764 | "KMB 0.000137 0.000083 0.000101 \n",
765 | "NVDA 0.001039 0.000320 0.000393 \n",
766 | "NFLX 0.000320 0.000840 0.000236 \n",
767 | "XRX 0.000393 0.000236 0.000830 "
768 | ]
769 | },
770 | "execution_count": 10,
771 | "metadata": {},
772 | "output_type": "execute_result"
773 | }
774 | ],
775 | "source": [
776 | "single_factor_covariance(df_returns, stocks, benchmark)"
777 | ]
778 | },
779 | {
780 | "cell_type": "markdown",
781 | "id": "32868fc0-c53a-4a32-9657-3546970c299a",
782 | "metadata": {},
783 | "source": [
784 | "### 2.3 The Common Correlation Model\n",
785 | "\n",
786 | "This model makes use of an estimate of each stock's risk $\\sigma_i$ and an average correlation between stocks $\\rho$. This implies that the covariance between two stocks is given by\n",
787 | "\n",
788 | "$$Cov(r_i, r_j) = \\sigma_i \\sigma_j \\rho $$"
789 | ]
790 | },
791 | {
792 | "cell_type": "code",
793 | "execution_count": 11,
794 | "id": "899f9f43-5205-41fd-a4c4-d18e48fbbe96",
795 | "metadata": {},
796 | "outputs": [],
797 | "source": [
798 | "def average_pairwise_correlation(df_returns):\n",
799 | " corrs = (numpy.triu(df_returns[stocks].corr()) - numpy.diag(numpy.ones(len(stocks)))).flatten()\n",
800 | " return corrs[corrs != 0].mean()"
801 | ]
802 | },
803 | {
804 | "cell_type": "code",
805 | "execution_count": 12,
806 | "id": "3b2ebdef-a500-46de-9504-f5c476d04e4a",
807 | "metadata": {},
808 | "outputs": [],
809 | "source": [
810 | "def common_correlation_covariance(df_returns, stocks, benchmark):\n",
811 | " \n",
812 | " df_risk_summary = calculate_risk_summary(df_returns, stocks, benchmark)\n",
813 | " avg_pairwise_corr = average_pairwise_correlation(df_returns[stocks])\n",
814 | " stock_risks = df_risk_summary[\"TotalRisk\"]\n",
815 | "\n",
816 | " cov = numpy.outer(stock_risks, stock_risks) * avg_pairwise_corr\n",
817 | "\n",
818 | " cov[numpy.diag_indices_from(cov)] = numpy.diag(cov) / avg_pairwise_corr\n",
819 | "\n",
820 | " return pandas.DataFrame(\n",
821 | " data=cov,\n",
822 | " index=df_risk_summary.index,\n",
823 | " columns=df_risk_summary.index\n",
824 | " )"
825 | ]
826 | },
827 | {
828 | "cell_type": "code",
829 | "execution_count": 13,
830 | "id": "fe6eae89-ec76-4327-88c1-c8c0ea0b04d3",
831 | "metadata": {},
832 | "outputs": [
833 | {
834 | "data": {
835 | "text/html": [
836 | "\n",
837 | "\n",
850 | "
\n",
851 | " \n",
852 | " \n",
853 | " Stock | \n",
854 | " ACN | \n",
855 | " BA | \n",
856 | " BAC | \n",
857 | " CAT | \n",
858 | " HP | \n",
859 | " INTC | \n",
860 | " KMB | \n",
861 | " NVDA | \n",
862 | " NFLX | \n",
863 | " XRX | \n",
864 | "
\n",
865 | " \n",
866 | " Stock | \n",
867 | " | \n",
868 | " | \n",
869 | " | \n",
870 | " | \n",
871 | " | \n",
872 | " | \n",
873 | " | \n",
874 | " | \n",
875 | " | \n",
876 | " | \n",
877 | "
\n",
878 | " \n",
879 | " \n",
880 | " \n",
881 | " ACN | \n",
882 | " 0.000316 | \n",
883 | " 0.000218 | \n",
884 | " 0.000153 | \n",
885 | " 0.000138 | \n",
886 | " 0.000259 | \n",
887 | " 0.000167 | \n",
888 | " 0.000093 | \n",
889 | " 0.000220 | \n",
890 | " 0.000198 | \n",
891 | " 0.000196 | \n",
892 | "
\n",
893 | " \n",
894 | " BA | \n",
895 | " 0.000218 | \n",
896 | " 0.001020 | \n",
897 | " 0.000275 | \n",
898 | " 0.000249 | \n",
899 | " 0.000466 | \n",
900 | " 0.000301 | \n",
901 | " 0.000167 | \n",
902 | " 0.000395 | \n",
903 | " 0.000355 | \n",
904 | " 0.000353 | \n",
905 | "
\n",
906 | " \n",
907 | " BAC | \n",
908 | " 0.000153 | \n",
909 | " 0.000275 | \n",
910 | " 0.000503 | \n",
911 | " 0.000175 | \n",
912 | " 0.000328 | \n",
913 | " 0.000211 | \n",
914 | " 0.000118 | \n",
915 | " 0.000277 | \n",
916 | " 0.000249 | \n",
917 | " 0.000248 | \n",
918 | "
\n",
919 | " \n",
920 | " CAT | \n",
921 | " 0.000138 | \n",
922 | " 0.000249 | \n",
923 | " 0.000175 | \n",
924 | " 0.000413 | \n",
925 | " 0.000296 | \n",
926 | " 0.000191 | \n",
927 | " 0.000106 | \n",
928 | " 0.000251 | \n",
929 | " 0.000226 | \n",
930 | " 0.000224 | \n",
931 | "
\n",
932 | " \n",
933 | " HP | \n",
934 | " 0.000259 | \n",
935 | " 0.000466 | \n",
936 | " 0.000328 | \n",
937 | " 0.000296 | \n",
938 | " 0.001450 | \n",
939 | " 0.000359 | \n",
940 | " 0.000199 | \n",
941 | " 0.000470 | \n",
942 | " 0.000423 | \n",
943 | " 0.000421 | \n",
944 | "
\n",
945 | " \n",
946 | " INTC | \n",
947 | " 0.000167 | \n",
948 | " 0.000301 | \n",
949 | " 0.000211 | \n",
950 | " 0.000191 | \n",
951 | " 0.000359 | \n",
952 | " 0.000604 | \n",
953 | " 0.000129 | \n",
954 | " 0.000304 | \n",
955 | " 0.000273 | \n",
956 | " 0.000271 | \n",
957 | "
\n",
958 | " \n",
959 | " KMB | \n",
960 | " 0.000093 | \n",
961 | " 0.000167 | \n",
962 | " 0.000118 | \n",
963 | " 0.000106 | \n",
964 | " 0.000199 | \n",
965 | " 0.000129 | \n",
966 | " 0.000187 | \n",
967 | " 0.000169 | \n",
968 | " 0.000152 | \n",
969 | " 0.000151 | \n",
970 | "
\n",
971 | " \n",
972 | " NVDA | \n",
973 | " 0.000220 | \n",
974 | " 0.000395 | \n",
975 | " 0.000277 | \n",
976 | " 0.000251 | \n",
977 | " 0.000470 | \n",
978 | " 0.000304 | \n",
979 | " 0.000169 | \n",
980 | " 0.001039 | \n",
981 | " 0.000358 | \n",
982 | " 0.000356 | \n",
983 | "
\n",
984 | " \n",
985 | " NFLX | \n",
986 | " 0.000198 | \n",
987 | " 0.000355 | \n",
988 | " 0.000249 | \n",
989 | " 0.000226 | \n",
990 | " 0.000423 | \n",
991 | " 0.000273 | \n",
992 | " 0.000152 | \n",
993 | " 0.000358 | \n",
994 | " 0.000840 | \n",
995 | " 0.000320 | \n",
996 | "
\n",
997 | " \n",
998 | " XRX | \n",
999 | " 0.000196 | \n",
1000 | " 0.000353 | \n",
1001 | " 0.000248 | \n",
1002 | " 0.000224 | \n",
1003 | " 0.000421 | \n",
1004 | " 0.000271 | \n",
1005 | " 0.000151 | \n",
1006 | " 0.000356 | \n",
1007 | " 0.000320 | \n",
1008 | " 0.000830 | \n",
1009 | "
\n",
1010 | " \n",
1011 | "
\n",
1012 | "
"
1013 | ],
1014 | "text/plain": [
1015 | "Stock ACN BA BAC CAT HP INTC KMB \\\n",
1016 | "Stock \n",
1017 | "ACN 0.000316 0.000218 0.000153 0.000138 0.000259 0.000167 0.000093 \n",
1018 | "BA 0.000218 0.001020 0.000275 0.000249 0.000466 0.000301 0.000167 \n",
1019 | "BAC 0.000153 0.000275 0.000503 0.000175 0.000328 0.000211 0.000118 \n",
1020 | "CAT 0.000138 0.000249 0.000175 0.000413 0.000296 0.000191 0.000106 \n",
1021 | "HP 0.000259 0.000466 0.000328 0.000296 0.001450 0.000359 0.000199 \n",
1022 | "INTC 0.000167 0.000301 0.000211 0.000191 0.000359 0.000604 0.000129 \n",
1023 | "KMB 0.000093 0.000167 0.000118 0.000106 0.000199 0.000129 0.000187 \n",
1024 | "NVDA 0.000220 0.000395 0.000277 0.000251 0.000470 0.000304 0.000169 \n",
1025 | "NFLX 0.000198 0.000355 0.000249 0.000226 0.000423 0.000273 0.000152 \n",
1026 | "XRX 0.000196 0.000353 0.000248 0.000224 0.000421 0.000271 0.000151 \n",
1027 | "\n",
1028 | "Stock NVDA NFLX XRX \n",
1029 | "Stock \n",
1030 | "ACN 0.000220 0.000198 0.000196 \n",
1031 | "BA 0.000395 0.000355 0.000353 \n",
1032 | "BAC 0.000277 0.000249 0.000248 \n",
1033 | "CAT 0.000251 0.000226 0.000224 \n",
1034 | "HP 0.000470 0.000423 0.000421 \n",
1035 | "INTC 0.000304 0.000273 0.000271 \n",
1036 | "KMB 0.000169 0.000152 0.000151 \n",
1037 | "NVDA 0.001039 0.000358 0.000356 \n",
1038 | "NFLX 0.000358 0.000840 0.000320 \n",
1039 | "XRX 0.000356 0.000320 0.000830 "
1040 | ]
1041 | },
1042 | "execution_count": 13,
1043 | "metadata": {},
1044 | "output_type": "execute_result"
1045 | }
1046 | ],
1047 | "source": [
1048 | "common_correlation_covariance(df_returns, stocks, benchmark)"
1049 | ]
1050 | },
1051 | {
1052 | "cell_type": "markdown",
1053 | "id": "2b2a389a-cf38-4d7a-b5e2-225a8fab709f",
1054 | "metadata": {},
1055 | "source": [
1056 | "## 2.4 Out of Sample Performance\n",
1057 | "\n",
1058 | "We compare the out of sample performance of the different risk models by comparing the estimated covaraince matrix on day $T$ with the realised covariance matrix over a subsequent period from $T + 1$ to $T + N$."
1059 | ]
1060 | },
1061 | {
1062 | "cell_type": "code",
1063 | "execution_count": 14,
1064 | "id": "49106e83-2b5a-48c7-b216-1b1afd080c20",
1065 | "metadata": {},
1066 | "outputs": [],
1067 | "source": [
1068 | "dates = df_returns.index.tolist()\n",
1069 | "train_window = 126\n",
1070 | "test_window = 21"
1071 | ]
1072 | },
1073 | {
1074 | "cell_type": "code",
1075 | "execution_count": 15,
1076 | "id": "333dd454-7d49-4175-8a49-63048b55da3d",
1077 | "metadata": {},
1078 | "outputs": [],
1079 | "source": [
1080 | "def norm(cov_train, cov_test):\n",
1081 | " \"\"\"Sum of squares\"\"\"\n",
1082 | " return numpy.power(cov_test - cov_train, 2).sum().sum()"
1083 | ]
1084 | },
1085 | {
1086 | "cell_type": "code",
1087 | "execution_count": 16,
1088 | "id": "5fdd1101-fa7e-4ed3-a8a1-e3bff3194c25",
1089 | "metadata": {},
1090 | "outputs": [],
1091 | "source": [
1092 | "models = {\n",
1093 | " \"Empirical\": cov_empirical,\n",
1094 | " \"SingleFactor\": single_factor_covariance,\n",
1095 | " \"CommonCorrelation\": common_correlation_covariance,\n",
1096 | "}"
1097 | ]
1098 | },
1099 | {
1100 | "cell_type": "code",
1101 | "execution_count": 17,
1102 | "id": "9596a695-b9f2-48fa-9276-93d787376e70",
1103 | "metadata": {},
1104 | "outputs": [],
1105 | "source": [
1106 | "out = []\n",
1107 | "\n",
1108 | "for ix in range(train_window, len(dates) - test_window):\n",
1109 | "\n",
1110 | " df_returns_train = df_returns.iloc[ix-train_window:ix]\n",
1111 | " df_returns_test = df_returns.iloc[ix:ix+test_window]\n",
1112 | "\n",
1113 | " cov_test = cov_empirical(df_returns_test, stocks, benchmark)\n",
1114 | "\n",
1115 | " for name, model in models.items():\n",
1116 | " cov_tr = model(df_returns_train, stocks, benchmark)\n",
1117 | " err = norm(cov_tr, cov_test)\n",
1118 | "\n",
1119 | " out.append({\n",
1120 | " \"Date\": dates[ix],\n",
1121 | " \"Model\": name,\n",
1122 | " \"Error\": err\n",
1123 | " })\n",
1124 | "\n",
1125 | "df_errors = (\n",
1126 | " pandas.DataFrame(out)\n",
1127 | " .set_index(keys=[\"Date\", \"Model\"])\n",
1128 | " .unstack(1)\n",
1129 | " [\"Error\"]\n",
1130 | ")"
1131 | ]
1132 | },
1133 | {
1134 | "cell_type": "code",
1135 | "execution_count": 18,
1136 | "id": "200449a7-f415-4e48-8ec3-fe9e075d5beb",
1137 | "metadata": {},
1138 | "outputs": [
1139 | {
1140 | "data": {
1141 | "image/png": "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a3j22WeZPXs2L730EieeeCJdunTh9NNPZ/78+UyZMqVBY7311lucd955hIWF8cwzz3j+Hu+//z5du3bFYFAy5BQVFXH99deTkJBAZGQkp556Khs2bKjxf7N27VpOP/104uPjiYqKYsyYMaxfv95zvEuXLgBMnjwZSZI821XdAJxOJ08++SQdO3ZEr9czZMgQFi5c6Dm+f/9+JEni22+/Zdy4cZhMJgYPHszKlStrnFuwEGJV0GzsyfzX8/r2HrcSGZXodzw+riNJdiW4KmPXkpacmkAgEIQcWZapsNpD8iPL9Xe/KigoYOHChcyYMYOwsLBqx6Ojo3E6nZx//vkUFBTw999/88cff7B3714uvfRSv7Z79uzh119/ZeHChXzxxRd88MEHnH322Rw+fJi///6bF154gYcffpjVq1f79Xvqqae45ppryMjIoE+fPlxxxRXccMMNPPDAA6xbtw5Zlrnllls87b/77jtuv/127rrrLjZv3swNN9zAtGnT+Ouvv/zO+8QTT3DJJZewceNGzjrrLK688krP8v68efM8gjoQ0dHRDRrr8ccfZ/LkyWzatIlrr70WgN27dzN//ny+/fZbMjIyALj44ovJycnh119/5d9//2XYsGGcdtppNbodlJaWMmXKFJYvX86qVavo2bMnZ511FqWlpQCewPOPPvqIo0eP1hiI/uqrrzJ79mxmzZrFxo0bmTBhAueddx67du3ya/fQQw9x9913k5GRQa9evbj88sux25u3uI+mWc8uOKbJLj8AKhhrieWMcTcFbJNk15GtsbP76HrgupadoEAgEISQSpuDfo/+FpKxtz45AZOufhJg9+7dyLJMnz59amyzePFiNm3axL59+0hLSwPgk08+oX///qxdu9aTVtLpdPLhhx8SERFBv379GDduHDt27OCXX35BpVLRu3dvXnjhBf766y9GjRrlOf+0adO45JJLALjvvvs44YQTeOSRR5gwYQIAt99+O9OmTfO0nzVrFlOnTvUITbcFeNasWYwbN87TburUqVx++eUAPPvss7z22musWbOGM888k127dtGtWze0Wm2tf5/6jnXFFVf4zRGUpf9PPvmEhIQEAJYvX86aNWvIyclBr9d7zv/999/zzTff8H//93/Vxj/11FP9tt99912io6P5+++/Oeecczznjo6OJjk5udb3cd9993HZZZcB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Nfa7++67OfvsswF44okn6N+/P7t376ZPnz68/vrrTJw4kbvvvhuAXr168c8///Dzzz/XeL7HHnuM2bNnc8EFFwCKtXPr1q288847fmL18ssvR61We7Y/++wzJk2a5BkL4NZbb+W3337j66+/ZuTIkZSWlvLqq6/yxhtveM7VvXt3j79rQkICAHFxcX4uD7NmzeK+++7jsssuA+CFF17gr7/+Ys6cOaSnp3va3XHHHZ55CxqOcAMQBB2762Fd30DLqkr4rAoEAkGr5MILLyQzM5Mff/yRM888kyVLljBs2DDmzp1bYx9fIZuSkgJATk4OADt27PBYNN1U3falvLycPXv2cN1113ksveHh4Tz99NPs2bPHr+0rr7xCRkaG5+f000/H4XDw1FNPMXDgQGJjYwkPD+e3337j4EGlCM22bduwWCycdtpp9f6blJSUkJmZyejRo/32jx49mm3btvntGz58eL3PK6hOu7Gspqenk56ejsPhqLuxoFmxuX4bdA0Uq2ohVgUCwbGDUWNk9RWrQzZ2QzEYDJx++umcfvrpPPLII1x//fU89thjTJ06NWB73yAit8uB09m4b/iysjJA8XMdNcrfGu1rRQUl2KtHjx5++55//nleffVV5syZw8CBAwkLC+OOO+7AalVyfBuNDf97NISwsLBmPX97p92I1RkzZjBjxgxKSkqIioqqu4Og2bC7vpQMuka6AQR9RgKBQND6kCSp3kvxrZF+/fo1Ordq7969q+VFry1PelJSEqmpqezdu5crr7yyweOtWLGC888/n6uuugpQRPPOnTvp168fAD179sRoNLJ48WKuv/76av3dPqq+BrHIyEhSU1NZsWIFY8aM8RurNiuxoOG0G7EqaD3YXG4ABn3DniQ1auUpXPisCgQCQeshPz+fiy++mGuvvZZBgwYRERHBunXrePHFFzn//PMbdc5bb72VU045hZdffplzzz2XP//8k19//bXWoK8nnniC2267jaioKM4880wsFgvr1q2jsLCQmTNn1jpez549+eabb/jnn3+IiYnh5ZdfJjs72yNWDQYD9913H/feey86nY7Ro0eTm5vLli1buO6660hMTMRoNLJw4UI6duyIwWAgKiqKe+65h8cee4zu3bszZMgQPvroIzIyMkTEf5ARYlUQVOx2Gw63ZbWBYlXtSV0lEAgEgtZCeHg4o0aN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",
1142 | "text/plain": [
1143 | ""
1144 | ]
1145 | },
1146 | "metadata": {},
1147 | "output_type": "display_data"
1148 | }
1149 | ],
1150 | "source": [
1151 | "f, ax = plt.subplots(1, 1, figsize=(8, 4))\n",
1152 | "\n",
1153 | "df_errors.plot(ax=ax)\n",
1154 | "\n",
1155 | "ax.set_yscale(\"log\")"
1156 | ]
1157 | },
1158 | {
1159 | "cell_type": "markdown",
1160 | "id": "2b4d004c-5c78-4458-bde0-890b8e1d2553",
1161 | "metadata": {},
1162 | "source": [
1163 | "The elementary risk models are comparable to the empirical covariance matrix calculation with respect to estimating the out of sample covariance matrix."
1164 | ]
1165 | },
1166 | {
1167 | "cell_type": "code",
1168 | "execution_count": null,
1169 | "id": "7a48c46c-aa0f-46a0-b67e-ea6d7218393a",
1170 | "metadata": {},
1171 | "outputs": [],
1172 | "source": []
1173 | }
1174 | ],
1175 | "metadata": {
1176 | "kernelspec": {
1177 | "display_name": "Python 3 (ipykernel)",
1178 | "language": "python",
1179 | "name": "python3"
1180 | },
1181 | "language_info": {
1182 | "codemirror_mode": {
1183 | "name": "ipython",
1184 | "version": 3
1185 | },
1186 | "file_extension": ".py",
1187 | "mimetype": "text/x-python",
1188 | "name": "python",
1189 | "nbconvert_exporter": "python",
1190 | "pygments_lexer": "ipython3",
1191 | "version": "3.11.5"
1192 | }
1193 | },
1194 | "nbformat": 4,
1195 | "nbformat_minor": 5
1196 | }
1197 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | tests_require = [
4 | "pytest>=6.1.1",
5 | "coverage>=5.3",
6 | "pytest-cov>=2.10.1",
7 | "pytest-cases==3.6.9",
8 | ]
9 |
10 | setuptools.setup(
11 | name="equity-risk-model",
12 | version="0.0.3",
13 | description="Portfolio analysis using an equity multi-factor risk model.",
14 | packages=setuptools.find_packages(),
15 | install_requires=["cvxpy", "numpy", "pandas", "scipy"],
16 | tests_require=tests_require,
17 | extras_requires={"test": tests_require},
18 | )
19 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blaahhrrgg/equity-risk-model/ba970af746640a4d03d67d825258cf276808865f/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import equity_risk_model
2 | import numpy
3 | import pandas
4 |
5 | from pytest_cases import fixture
6 |
7 |
8 | @fixture(scope="module")
9 | def factor_model():
10 |
11 | universe = numpy.array(["A", "B", "C", "D", "E"])
12 | factors = numpy.array(["foo", "bar", "baz"])
13 |
14 | factor_loadings = pandas.DataFrame(
15 | data=numpy.array(
16 | [
17 | [0.2, 0.3, -0.1, -0.2, 0.45],
18 | [0.01, -0.2, -0.23, -0.01, 0.4],
19 | [0.1, 0.05, 0.23, 0.15, -0.1],
20 | ]
21 | ),
22 | columns=universe,
23 | index=factors,
24 | )
25 |
26 | covariance_factor = pandas.DataFrame(
27 | data=numpy.array(
28 | [[0.3, 0.05, 0.01], [0.05, 0.15, -0.10], [0.01, -0.10, 0.2]]
29 | ),
30 | columns=factors,
31 | index=factors,
32 | )
33 |
34 | covariance_specific = pandas.DataFrame(
35 | data=numpy.diag([0.05, 0.04, 0.10, 0.02, 0.09]),
36 | index=universe,
37 | columns=universe,
38 | )
39 |
40 | return equity_risk_model.model.FactorRiskModel(
41 | universe,
42 | factors,
43 | factor_loadings,
44 | covariance_factor,
45 | covariance_specific,
46 | )
47 |
48 |
49 | @fixture(scope="module")
50 | def factor_model_with_groups():
51 |
52 | universe = numpy.array(["A", "B", "C", "D", "E"])
53 | factors = numpy.array(["foo", "bar", "baz"])
54 |
55 | factor_loadings = pandas.DataFrame(
56 | data=[
57 | [0.2, 0.3, -0.1, -0.2, 0.45],
58 | [0.01, -0.2, -0.23, -0.01, 0.4],
59 | [0.1, 0.05, 0.23, 0.15, -0.1],
60 | ],
61 | index=factors,
62 | columns=universe,
63 | )
64 |
65 | covariance_factor = pandas.DataFrame(
66 | data=[[0.3, 0.05, 0.01], [0.05, 0.15, -0.10], [0.01, -0.10, 0.2]],
67 | index=factors,
68 | columns=factors,
69 | )
70 |
71 | covariance_specific = pandas.DataFrame(
72 | data=numpy.diag([0.05, 0.04, 0.10, 0.02, 0.09]),
73 | index=universe,
74 | columns=universe,
75 | )
76 |
77 | factor_groups = {"Alpha": ["foo", "bar"], "Beta": ["baz"]}
78 |
79 | return equity_risk_model.model.FactorRiskModel(
80 | universe,
81 | factors,
82 | factor_loadings,
83 | covariance_factor,
84 | covariance_specific,
85 | factor_group_mapping=factor_groups,
86 | )
87 |
88 |
89 | @fixture(scope="module")
90 | def risk_calculator(factor_model):
91 |
92 | return equity_risk_model.risk.RiskCalculator(factor_model)
93 |
94 |
95 | @fixture(scope="module")
96 | def risk_calculator_with_factor_groups(factor_model_with_groups):
97 |
98 | return equity_risk_model.risk.RiskCalculator(factor_model_with_groups)
99 |
100 |
101 | @fixture(scope="module")
102 | def concentration_calculator(risk_calculator):
103 |
104 | return equity_risk_model.concentration.ConcentrationCalculator(
105 | risk_calculator=risk_calculator
106 | )
107 |
--------------------------------------------------------------------------------
/tests/test_concentration.py:
--------------------------------------------------------------------------------
1 | import numpy
2 | import pandas
3 | import pytest
4 |
5 | weights_concentrated = numpy.array([1, 0, 0, 0, 0])
6 | weights_equal = numpy.array([0.2, 0.2, 0.2, 0.2, 0.2])
7 | weights_longshort1 = numpy.array([-0.5, 0.5, 0.5, 0.5])
8 | weights_longshort2 = numpy.array([-1, 2 / 3.0, 2 / 3.0, 2 / 3.0])
9 |
10 |
11 | @pytest.mark.parametrize(
12 | "weights, expected",
13 | [
14 | (weights_concentrated, 1),
15 | (weights_equal, 5),
16 | (weights_longshort1, 1),
17 | (weights_longshort2, 3 / 7.0),
18 | ],
19 | )
20 | def test_enc(weights, expected, concentration_calculator):
21 |
22 | numpy.testing.assert_almost_equal(
23 | concentration_calculator.enc(weights), expected
24 | )
25 |
26 |
27 | @pytest.mark.parametrize(
28 | "weights, expected",
29 | [
30 | (weights_equal, 5),
31 | ],
32 | )
33 | def test_entropy(weights, expected, concentration_calculator):
34 |
35 | numpy.testing.assert_almost_equal(
36 | concentration_calculator.entropy(weights), expected
37 | )
38 |
39 |
40 | @pytest.mark.parametrize(
41 | "weights, expected",
42 | [(weights_equal, 3.7346305378867153), (weights_concentrated, 1)],
43 | )
44 | def test_effective_number_of_correlated_bets(
45 | weights, expected, concentration_calculator
46 | ):
47 |
48 | p = pandas.Series(
49 | data=weights,
50 | index=concentration_calculator.risk_calculator.factor_model.universe,
51 | )
52 |
53 | numpy.testing.assert_almost_equal(
54 | concentration_calculator.number_of_correlated_bets(p),
55 | expected,
56 | )
57 |
58 |
59 | @pytest.mark.parametrize(
60 | "weights, expected",
61 | [(weights_equal, 3.9823008849557513), (weights_concentrated, 1)],
62 | )
63 | def test_effective_number_of_uncorrelated_bets(
64 | weights, expected, concentration_calculator
65 | ):
66 |
67 | p = pandas.Series(
68 | data=weights,
69 | index=concentration_calculator.risk_calculator.factor_model.universe,
70 | )
71 |
72 | numpy.testing.assert_almost_equal(
73 | concentration_calculator.number_of_uncorrelated_bets(p),
74 | expected,
75 | )
76 |
77 |
78 | @pytest.mark.parametrize(
79 | "weights, expected",
80 | [
81 | (weights_equal, 2),
82 | (weights_concentrated, 1),
83 | ],
84 | )
85 | def test_min_assets(weights, expected, concentration_calculator):
86 |
87 | p = pandas.Series(
88 | data=weights,
89 | index=concentration_calculator.risk_calculator.factor_model.universe,
90 | )
91 |
92 | numpy.testing.assert_almost_equal(
93 | concentration_calculator.min_assets_for_mcsr_threshold(p),
94 | expected,
95 | )
96 |
97 |
98 | @pytest.mark.parametrize(
99 | "weights",
100 | [(weights_equal)],
101 | )
102 | def test_summarise(weights, concentration_calculator):
103 |
104 | p = pandas.Series(
105 | data=weights,
106 | index=concentration_calculator.risk_calculator.factor_model.universe,
107 | )
108 |
109 | out = concentration_calculator.summarise_portfolio(p)
110 |
111 | assert isinstance(out, dict)
112 | assert out["NAssets"] == 5
113 |
--------------------------------------------------------------------------------
/tests/test_correlation.py:
--------------------------------------------------------------------------------
1 | import equity_risk_model
2 | import numpy
3 | import pytest
4 |
5 |
6 | @pytest.mark.parametrize(
7 | "matrix, expected",
8 | [
9 | (numpy.diag([0.1, 0.2, 0.3]), True), # Positive eigenvalues
10 | (numpy.diag([-0.1, -0.2, -0.3]), False), # Negative eigenvalues
11 | (numpy.diag([[9, 7], [6, 14]]), False), # Non-symmetric
12 | ],
13 | )
14 | def test_is_positive_semidefinite(matrix, expected):
15 |
16 | assert (
17 | equity_risk_model.correlation.is_positive_semidefinite(matrix)
18 | == expected
19 | )
20 |
--------------------------------------------------------------------------------
/tests/test_optimiser.py:
--------------------------------------------------------------------------------
1 | import equity_risk_model
2 | import pandas
3 | import numpy
4 | import pytest
5 |
6 |
7 | def test_abstract_methods():
8 | with pytest.raises(NotImplementedError):
9 | equity_risk_model.optimiser.PortfolioOptimiser._objective_function(
10 | None
11 | )
12 |
13 | with pytest.raises(NotImplementedError):
14 | equity_risk_model.optimiser.PortfolioOptimiser._constraints(None)
15 |
16 |
17 | def test_min_variance(factor_model):
18 |
19 | opt = equity_risk_model.optimiser.MinimumVariance(factor_model)
20 | opt.solve()
21 |
22 | numpy.testing.assert_almost_equal(
23 | opt.x.value,
24 | numpy.array([0.1502093, 0.1485704, 0.0598607, 0.5079278, 0.1334318]),
25 | )
26 |
27 |
28 | def test_max_sharpe(factor_model):
29 |
30 | expected_returns = numpy.array([0.2, 0.1, 0.05, 0.1, 0.2])
31 |
32 | opt = equity_risk_model.optimiser.MaximumSharpe(
33 | factor_model, expected_returns
34 | )
35 | opt.solve()
36 |
37 | numpy.testing.assert_almost_equal(
38 | opt.x.value,
39 | numpy.array([0.82123741, 0, 0, 0, 0.17876259]),
40 | )
41 |
42 |
43 | def test_proportional_factor_neutral(factor_model):
44 |
45 | expected_returns = numpy.array([0.2, 0.1, 0.05, 0.1, 0.2])
46 |
47 | opt = equity_risk_model.optimiser.ProportionalFactorNeutral(
48 | factor_model, expected_returns
49 | )
50 | opt.solve()
51 |
52 | # Check weights
53 | numpy.testing.assert_almost_equal(
54 | opt.x.value,
55 | numpy.array([0.0280274, 0.016464, -0.0464042, 0.0347927, -0.0182813]),
56 | )
57 |
58 | # Validate factor risk is zero
59 | w_opt = pandas.Series(data=opt.x.value, index=factor_model.universe)
60 |
61 | factor_risks = equity_risk_model.risk.RiskCalculator(
62 | factor_model
63 | ).factor_risks(w_opt)
64 |
65 | numpy.testing.assert_almost_equal(
66 | factor_risks, numpy.zeros((factor_model.n_factors))
67 | )
68 |
69 |
70 | def test_internally_hedged_factor_neutral(factor_model):
71 |
72 | initial_weights = pandas.Series(
73 | data=[-0.2, -0.2, 0.2, -0.2, 0.2], index=factor_model.universe
74 | )
75 |
76 | opt = equity_risk_model.optimiser.InternallyHedgedFactorNeutral(
77 | factor_model, initial_weights
78 | )
79 | opt.solve()
80 |
81 | # Check weights
82 | numpy.testing.assert_almost_equal(
83 | opt.x.value,
84 | numpy.array([0.0144357, 0.1267867, 0.0577181, 0.0275523, -0.0880908]),
85 | )
86 |
87 | # Check sign of weights has not changed
88 | opt_weights = pandas.Series(opt.x.value, index=factor_model.universe)
89 |
90 | numpy.testing.assert_array_equal(
91 | numpy.sign(initial_weights + opt_weights), numpy.sign(initial_weights)
92 | )
93 |
94 | # Validate factor risk is zero
95 | factor_risks = equity_risk_model.risk.RiskCalculator(
96 | factor_model
97 | ).factor_risks(opt.x.value + initial_weights)
98 |
99 | numpy.testing.assert_almost_equal(
100 | factor_risks, numpy.zeros((factor_model.n_factors))
101 | )
102 |
103 |
104 | def test_internally_hedged_factor_tolerant(factor_model):
105 |
106 | initial_weights = pandas.Series(
107 | data=[0.2, 0.2, 0.2, 0.2, 0.2], index=factor_model.universe
108 | )
109 |
110 | factor_risk_upper_bounds = numpy.array([0.01, 0.01, 0.01])
111 |
112 | opt = equity_risk_model.optimiser.InternallyHedgedFactorTolerant(
113 | factor_model, initial_weights, factor_risk_upper_bounds
114 | )
115 | opt.solve()
116 |
117 | # Check weights
118 | numpy.testing.assert_almost_equal(
119 | opt.x.value,
120 | numpy.array(
121 | [-0.2030572, -0.2315047, -0.0851033, -0.1368858, -0.0834827]
122 | ),
123 | )
124 |
125 | # Validate factor risks are less than specified upper bound
126 | factor_risks = equity_risk_model.risk.RiskCalculator(
127 | factor_model
128 | ).factor_risks(opt.x.value + initial_weights)
129 |
130 | numpy.testing.assert_array_less(
131 | factor_risks - factor_risk_upper_bounds,
132 | numpy.ones(factor_model.n_factors) * 1e-9,
133 | )
134 |
--------------------------------------------------------------------------------
/tests/test_risk.py:
--------------------------------------------------------------------------------
1 | import numpy
2 | import pandas
3 | import pytest
4 |
5 |
6 | @pytest.mark.parametrize(
7 | "weights, expected_total, expected_factor, expected_specific",
8 | [
9 | (
10 | [0.2] * 5,
11 | 0.13712548997177731,
12 | 0.08248272546418432,
13 | 0.10954451150103323,
14 | ),
15 | ([0.0] * 5, 0.0, 0.0, 0.0),
16 | ],
17 | )
18 | def test_total_risk(
19 | weights,
20 | expected_total,
21 | expected_factor,
22 | expected_specific,
23 | risk_calculator,
24 | ):
25 |
26 | p = pandas.Series(
27 | data=weights, index=risk_calculator.factor_model.universe
28 | )
29 |
30 | numpy.testing.assert_almost_equal(
31 | risk_calculator.total_risk(p), expected_total
32 | )
33 |
34 | numpy.testing.assert_almost_equal(
35 | risk_calculator.total_factor_risk(p), expected_factor
36 | )
37 |
38 | numpy.testing.assert_almost_equal(
39 | risk_calculator.total_specific_risk(p), expected_specific
40 | )
41 |
42 |
43 | @pytest.mark.parametrize(
44 | "weights, expected",
45 | [
46 | (
47 | [0.2] * 5,
48 | numpy.array([0.0712039, -0.0023238, 0.0384604]),
49 | ),
50 | ([0.0] * 5, numpy.zeros(3)),
51 | ],
52 | )
53 | def test_factor_risks(weights, expected, risk_calculator):
54 |
55 | p = pandas.Series(
56 | data=weights, index=risk_calculator.factor_model.universe
57 | )
58 |
59 | numpy.testing.assert_almost_equal(
60 | risk_calculator.factor_risks(p), expected
61 | )
62 |
63 |
64 | @pytest.mark.parametrize(
65 | "weights, expected",
66 | [
67 | ([0.2] * 5, 0.015773395322504297),
68 | ([0.0] * 5, 0.0),
69 | ],
70 | )
71 | def test_factor_covariance(weights, expected, risk_calculator):
72 |
73 | p = pandas.Series(
74 | data=weights, index=risk_calculator.factor_model.universe
75 | )
76 |
77 | numpy.testing.assert_almost_equal(
78 | risk_calculator.factor_risk_covariance(p), expected
79 | )
80 |
81 |
82 | def test_contributions_to_total_risk(risk_calculator):
83 |
84 | expected = numpy.array(
85 | [0.028867, 0.0312458, 0.0308141, -0.0014833, 0.0476819]
86 | )
87 |
88 | p = pandas.Series(
89 | data=[0.2] * 5, index=risk_calculator.factor_model.universe
90 | )
91 |
92 | numpy.testing.assert_almost_equal(
93 | risk_calculator.contribution_to_total_risk(p),
94 | expected,
95 | )
96 |
97 |
98 | def test_contributions_to_total_factor_risk(risk_calculator):
99 |
100 | p = pandas.Series(
101 | data=[0.2] * 5, index=risk_calculator.factor_model.universe
102 | )
103 |
104 | expected = numpy.array(
105 | [0.0237432, 0.0325474, 0.0027327, -0.012165, 0.0356244]
106 | )
107 |
108 | numpy.testing.assert_almost_equal(
109 | risk_calculator.contribution_to_total_factor_risk(p),
110 | expected,
111 | )
112 |
113 |
114 | def test_contributions_to_total_specific_risk(risk_calculator):
115 |
116 | p = pandas.Series(
117 | data=[0.2] * 5, index=risk_calculator.factor_model.universe
118 | )
119 | expected = numpy.array(
120 | [0.0182574, 0.0146059, 0.0365148, 0.007303, 0.0328634]
121 | )
122 |
123 | numpy.testing.assert_almost_equal(
124 | risk_calculator.contribution_to_total_specific_risk(p),
125 | expected,
126 | )
127 |
128 |
129 | def test_contributions_to_factor_risks(risk_calculator):
130 |
131 | p = pandas.Series(
132 | data=[0.2] * 5, index=risk_calculator.factor_model.universe
133 | )
134 |
135 | numpy.testing.assert_almost_equal(
136 | numpy.sum(
137 | risk_calculator.contributions_to_factor_risks(p),
138 | axis=0,
139 | ).values,
140 | risk_calculator.factor_risks(p).values,
141 | )
142 |
143 |
144 | def test_factor_group_risk(risk_calculator_with_factor_groups):
145 |
146 | p = pandas.Series(
147 | data=[0.2] * 5,
148 | index=risk_calculator_with_factor_groups.factor_model.universe,
149 | )
150 |
151 | factor_group_risk = risk_calculator_with_factor_groups.factor_group_risks(
152 | p
153 | )
154 |
155 | # Check structure
156 | assert isinstance(factor_group_risk, dict)
157 | assert set(factor_group_risk.keys()) == set(
158 | risk_calculator_with_factor_groups.factor_model.factor_groups
159 | )
160 |
161 | # Check covariance
162 | factor_group_covariance = (
163 | risk_calculator_with_factor_groups.factor_group_covariance(p)
164 | )
165 |
166 | numpy.testing.assert_almost_equal(factor_group_covariance, 0.0180776)
167 |
168 |
169 | def test_factor_group_factor_risk(risk_calculator_with_factor_groups):
170 |
171 | p = pandas.Series(
172 | data=[0.2] * 5,
173 | index=risk_calculator_with_factor_groups.factor_model.universe,
174 | )
175 |
176 | factor_risks = risk_calculator_with_factor_groups.factor_risks(p)
177 |
178 | assert isinstance(factor_risks, pandas.Series)
179 |
180 | # Check index has correct structure
181 | assert isinstance(factor_risks.index, pandas.MultiIndex)
182 |
183 | assert set(factor_risks.index.get_level_values(0)) == set(
184 | risk_calculator_with_factor_groups.factor_model.factor_groups
185 | )
186 | assert set(factor_risks.index.get_level_values(1)) == set(
187 | risk_calculator_with_factor_groups.factor_model.factors
188 | )
189 |
--------------------------------------------------------------------------------
/tests/test_tearsheet.py:
--------------------------------------------------------------------------------
1 | import numpy
2 | import pandas
3 | import pytest
4 |
5 | import equity_risk_model
6 |
7 |
8 | @pytest.fixture(scope="class")
9 | def concentration_tearsheet(concentration_calculator):
10 | return equity_risk_model.tearsheet.ConcentrationTearsheet(
11 | concentration_calculator=concentration_calculator
12 | )
13 |
14 |
15 | @pytest.fixture(scope="class")
16 | def factor_risk_summary_tearsheet(risk_calculator_with_factor_groups):
17 | return equity_risk_model.tearsheet.FactorRiskSummaryTearsheet(
18 | risk_calculator=risk_calculator_with_factor_groups
19 | )
20 |
21 |
22 | @pytest.fixture(scope="class")
23 | def factor_group_risk_summary_tearsheet(risk_calculator_with_factor_groups):
24 | return equity_risk_model.tearsheet.FactorGroupRiskTearsheet(
25 | risk_calculator=risk_calculator_with_factor_groups
26 | )
27 |
28 |
29 | @pytest.fixture(scope="class")
30 | def factor_risk_tearsheet(risk_calculator_with_factor_groups):
31 | return equity_risk_model.tearsheet.FactorRiskTearsheet(
32 | risk_calculator=risk_calculator_with_factor_groups
33 | )
34 |
35 |
36 | @pytest.fixture(scope="class")
37 | def portfolio_weights(factor_model):
38 | return {
39 | "EqualWeights": pandas.Series(
40 | data=numpy.array([0.2, 0.2, 0.2, 0.2, 0.2]),
41 | index=factor_model.universe,
42 | ),
43 | "ConcentratedPortfolio": pandas.Series(
44 | data=numpy.array([1.0, 0.0, 0.0, 0.0, 0.0]),
45 | index=factor_model.universe,
46 | ),
47 | "LongShortPortfolio": pandas.Series(
48 | data=numpy.array([0.4, 0.4, 0.2, -0.6, -0.4]),
49 | index=factor_model.universe,
50 | ),
51 | }
52 |
53 |
54 | def test_base_class():
55 |
56 | # Check abstract method raises exception in base class
57 | with pytest.raises(NotImplementedError):
58 | equity_risk_model.tearsheet.BaseTearsheet().create_portfolio_panel(
59 | None
60 | )
61 |
62 |
63 | def test_concentration_tearsheet(portfolio_weights, concentration_tearsheet):
64 |
65 | tearsheet = concentration_tearsheet.create_tearsheet(portfolio_weights)
66 |
67 | # Check format of output
68 | assert isinstance(tearsheet, pandas.DataFrame)
69 | assert len(tearsheet.columns) == len(portfolio_weights.keys())
70 |
71 |
72 | def test_factor_risk_summary_tearsheet(
73 | portfolio_weights, factor_risk_summary_tearsheet
74 | ):
75 |
76 | tearsheet = factor_risk_summary_tearsheet.create_tearsheet(
77 | portfolio_weights
78 | )
79 |
80 | # Check format of output
81 | assert isinstance(tearsheet, pandas.DataFrame)
82 | assert len(tearsheet.columns) == len(portfolio_weights.keys())
83 |
84 |
85 | def test_factor_group_risk_tearsheet(
86 | portfolio_weights, factor_group_risk_summary_tearsheet
87 | ):
88 |
89 | tearsheet = factor_group_risk_summary_tearsheet.create_tearsheet(
90 | portfolio_weights
91 | )
92 |
93 | # Check format of output
94 | assert isinstance(tearsheet, pandas.DataFrame)
95 | assert len(tearsheet.columns) == len(portfolio_weights.keys())
96 |
97 |
98 | def test_factor_risk_tearsheet(portfolio_weights, factor_risk_tearsheet):
99 |
100 | tearsheet = factor_risk_tearsheet.create_tearsheet(portfolio_weights)
101 |
102 | # Check format of output
103 | assert isinstance(tearsheet, pandas.DataFrame)
104 | assert len(tearsheet.columns) == len(portfolio_weights.keys())
105 |
--------------------------------------------------------------------------------