├── .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 | " \n", 169 | " \n", 170 | " \n", 171 | " \n", 172 | " \n", 173 | " \n", 174 | " \n", 175 | " \n", 176 | " \n", 177 | " \n", 178 | " \n", 179 | " \n", 180 | " \n", 181 | " \n", 182 | " \n", 183 | " \n", 184 | " \n", 185 | " \n", 186 | " \n", 187 | " \n", 188 | " \n", 189 | " \n", 190 | " \n", 191 | " \n", 192 | " \n", 193 | " \n", 194 | " \n", 195 | " \n", 196 | " \n", 197 | " \n", 198 | " \n", 199 | " \n", 200 | " \n", 201 | " \n", 202 | " \n", 203 | " \n", 204 | " \n", 205 | " \n", 206 | " \n", 207 | " \n", 208 | " \n", 209 | " \n", 210 | " \n", 211 | " \n", 212 | " \n", 213 | " \n", 214 | " \n", 215 | " \n", 216 | " \n", 217 | " \n", 218 | " \n", 219 | " \n", 220 | " \n", 221 | " \n", 222 | " \n", 223 | " \n", 224 | " \n", 225 | " \n", 226 | " \n", 227 | " \n", 228 | " \n", 229 | " \n", 230 | " \n", 231 | " \n", 232 | " \n", 233 | " \n", 234 | " \n", 235 | " \n", 236 | " \n", 237 | " \n", 238 | " \n", 239 | " \n", 240 | " \n", 241 | "
BetaTotalRiskResidualRisk
Stock
ACN1.1055470.0177720.010200
BA1.4852890.0319340.025249
BAC1.2412150.0224380.015379
CAT0.9771790.0203120.015720
HP1.4992000.0380740.032560
INTC1.2286340.0245800.018509
KMB0.4524690.0136600.012293
NVDA1.7517600.0322280.022514
NFLX1.0533970.0289890.025457
XRX1.2938110.0288090.023236
\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 | " \n", 325 | " \n", 326 | " \n", 327 | " \n", 328 | " \n", 329 | " \n", 330 | " \n", 331 | " \n", 332 | " \n", 333 | " \n", 334 | " \n", 335 | " \n", 336 | " \n", 337 | " \n", 338 | " \n", 339 | " \n", 340 | " \n", 341 | " \n", 342 | " \n", 343 | " \n", 344 | " \n", 345 | " \n", 346 | " \n", 347 | " \n", 348 | " \n", 349 | " \n", 350 | " \n", 351 | " \n", 352 | " \n", 353 | " \n", 354 | " \n", 355 | " \n", 356 | " \n", 357 | " \n", 358 | " \n", 359 | " \n", 360 | " \n", 361 | " \n", 362 | " \n", 363 | " \n", 364 | " \n", 365 | " \n", 366 | " \n", 367 | " \n", 368 | " \n", 369 | " \n", 370 | " \n", 371 | " \n", 372 | " \n", 373 | " \n", 374 | " \n", 375 | " \n", 376 | " \n", 377 | " \n", 378 | " \n", 379 | " \n", 380 | " \n", 381 | " \n", 382 | " \n", 383 | " \n", 384 | " \n", 385 | " \n", 386 | " \n", 387 | " \n", 388 | " \n", 389 | " \n", 390 | " \n", 391 | " \n", 392 | " \n", 393 | " \n", 394 | " \n", 395 | " \n", 396 | " \n", 397 | " \n", 398 | " \n", 399 | " \n", 400 | " \n", 401 | " \n", 402 | " \n", 403 | " \n", 404 | " \n", 405 | " \n", 406 | " \n", 407 | " \n", 408 | " \n", 409 | " \n", 410 | " \n", 411 | " \n", 412 | " \n", 413 | " \n", 414 | " \n", 415 | " \n", 416 | " \n", 417 | " \n", 418 | " \n", 419 | " \n", 420 | " \n", 421 | " \n", 422 | " \n", 423 | " \n", 424 | " \n", 425 | " \n", 426 | " \n", 427 | " \n", 428 | " \n", 429 | " \n", 430 | " \n", 431 | " \n", 432 | " \n", 433 | " \n", 434 | " \n", 435 | " \n", 436 | " \n", 437 | " \n", 438 | " \n", 439 | " \n", 440 | " \n", 441 | " \n", 442 | " \n", 443 | " \n", 444 | " \n", 445 | " \n", 446 | " \n", 447 | " \n", 448 | " \n", 449 | " \n", 450 | " \n", 451 | " \n", 452 | " \n", 453 | " \n", 454 | " \n", 455 | " \n", 456 | " \n", 457 | " \n", 458 | " \n", 459 | " \n", 460 | " \n", 461 | " \n", 462 | " \n", 463 | " \n", 464 | " \n", 465 | " \n", 466 | " \n", 467 | " \n", 468 | "
ACNBABACCATHPINTCKMBNVDANFLXXRX
ACN0.0003160.0002590.0002280.0001790.0002510.0002230.0000950.0003240.0002060.000256
BA0.0002590.0010200.0004090.0003200.0005830.0003140.0000710.0003670.0002140.000472
BAC0.0002280.0004090.0005030.0003030.0004990.0002430.0000790.0002720.0001360.000376
CAT0.0001790.0003200.0003030.0004130.0004270.0002000.0000560.0002310.0001050.000324
HP0.0002510.0005830.0004990.0004270.0014500.0003230.0000630.0003260.0001510.000508
INTC0.0002230.0003140.0002430.0002000.0003230.0006040.0000870.0004330.0002610.000271
KMB0.0000950.0000710.0000790.0000560.0000630.0000870.0001870.0000750.0000560.000073
NVDA0.0003240.0003670.0002720.0002310.0003260.0004330.0000750.0010390.0004470.000310
NFLX0.0002060.0002140.0001360.0001050.0001510.0002610.0000560.0004470.0008400.000194
XRX0.0002560.0004720.0003760.0003240.0005080.0002710.0000730.0003100.0001940.000830
\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 | " \n", 582 | " \n", 583 | " \n", 584 | " \n", 585 | " \n", 586 | " \n", 587 | " \n", 588 | " \n", 589 | " \n", 590 | " \n", 591 | " \n", 592 | " \n", 593 | " \n", 594 | " \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 | " \n", 610 | " \n", 611 | " \n", 612 | " \n", 613 | " \n", 614 | " \n", 615 | " \n", 616 | " \n", 617 | " \n", 618 | " \n", 619 | " \n", 620 | " \n", 621 | " \n", 622 | " \n", 623 | " \n", 624 | " \n", 625 | " \n", 626 | " \n", 627 | " \n", 628 | " \n", 629 | " \n", 630 | " \n", 631 | " \n", 632 | " \n", 633 | " \n", 634 | " \n", 635 | " \n", 636 | " \n", 637 | " \n", 638 | " \n", 639 | " \n", 640 | " \n", 641 | " \n", 642 | " \n", 643 | " \n", 644 | " \n", 645 | " \n", 646 | " \n", 647 | " \n", 648 | " \n", 649 | " \n", 650 | " \n", 651 | " \n", 652 | " \n", 653 | " \n", 654 | " \n", 655 | " \n", 656 | " \n", 657 | " \n", 658 | " \n", 659 | " \n", 660 | " \n", 661 | " \n", 662 | " \n", 663 | " \n", 664 | " \n", 665 | " \n", 666 | " \n", 667 | " \n", 668 | " \n", 669 | " \n", 670 | " \n", 671 | " \n", 672 | " \n", 673 | " \n", 674 | " \n", 675 | " \n", 676 | " \n", 677 | " \n", 678 | " \n", 679 | " \n", 680 | " \n", 681 | " \n", 682 | " \n", 683 | " \n", 684 | " \n", 685 | " \n", 686 | " \n", 687 | " \n", 688 | " \n", 689 | " \n", 690 | " \n", 691 | " \n", 692 | " \n", 693 | " \n", 694 | " \n", 695 | " \n", 696 | " \n", 697 | " \n", 698 | " \n", 699 | " \n", 700 | " \n", 701 | " \n", 702 | " \n", 703 | " \n", 704 | " \n", 705 | " \n", 706 | " \n", 707 | " \n", 708 | " \n", 709 | " \n", 710 | " \n", 711 | " \n", 712 | " \n", 713 | " \n", 714 | " \n", 715 | " \n", 716 | " \n", 717 | " \n", 718 | " \n", 719 | " \n", 720 | " \n", 721 | " \n", 722 | " \n", 723 | " \n", 724 | " \n", 725 | " \n", 726 | " \n", 727 | " \n", 728 | " \n", 729 | " \n", 730 | " \n", 731 | " \n", 732 | " \n", 733 | " \n", 734 | " \n", 735 | " \n", 736 | " \n", 737 | " \n", 738 | " \n", 739 | "
StockACNBABACCATHPINTCKMBNVDANFLXXRX
Stock
ACN0.0003160.0002850.0002380.0001870.0002870.0002350.0000870.0003360.0002020.000248
BA0.0002850.0010200.0003190.0002520.0003860.0003160.0001160.0004510.0002710.000333
BAC0.0002380.0003190.0005030.0002100.0003220.0002640.0000970.0003770.0002270.000278
CAT0.0001870.0002520.0002100.0004130.0002540.0002080.0000770.0002970.0001780.000219
HP0.0002870.0003860.0003220.0002540.0014500.0003190.0001180.0004550.0002740.000336
INTC0.0002350.0003160.0002640.0002080.0003190.0006040.0000960.0003730.0002240.000275
KMB0.0000870.0001160.0000970.0000770.0001180.0000960.0001870.0001370.0000830.000101
NVDA0.0003360.0004510.0003770.0002970.0004550.0003730.0001370.0010390.0003200.000393
NFLX0.0002020.0002710.0002270.0001780.0002740.0002240.0000830.0003200.0008400.000236
XRX0.0002480.0003330.0002780.0002190.0003360.0002750.0001010.0003930.0002360.000830
\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 | " \n", 854 | " \n", 855 | " \n", 856 | " \n", 857 | " \n", 858 | " \n", 859 | " \n", 860 | " \n", 861 | " \n", 862 | " \n", 863 | " \n", 864 | " \n", 865 | " \n", 866 | " \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 | " \n", 882 | " \n", 883 | " \n", 884 | " \n", 885 | " \n", 886 | " \n", 887 | " \n", 888 | " \n", 889 | " \n", 890 | " \n", 891 | " \n", 892 | " \n", 893 | " \n", 894 | " \n", 895 | " \n", 896 | " \n", 897 | " \n", 898 | " \n", 899 | " \n", 900 | " \n", 901 | " \n", 902 | " \n", 903 | " \n", 904 | " \n", 905 | " \n", 906 | " \n", 907 | " \n", 908 | " \n", 909 | " \n", 910 | " \n", 911 | " \n", 912 | " \n", 913 | " \n", 914 | " \n", 915 | " \n", 916 | " \n", 917 | " \n", 918 | " \n", 919 | " \n", 920 | " \n", 921 | " \n", 922 | " \n", 923 | " \n", 924 | " \n", 925 | " \n", 926 | " \n", 927 | " \n", 928 | " \n", 929 | " \n", 930 | " \n", 931 | " \n", 932 | " \n", 933 | " \n", 934 | " \n", 935 | " \n", 936 | " \n", 937 | " \n", 938 | " \n", 939 | " \n", 940 | " \n", 941 | " \n", 942 | " \n", 943 | " \n", 944 | " \n", 945 | " \n", 946 | " \n", 947 | " \n", 948 | " \n", 949 | " \n", 950 | " \n", 951 | " \n", 952 | " \n", 953 | " \n", 954 | " \n", 955 | " \n", 956 | " \n", 957 | " \n", 958 | " \n", 959 | " \n", 960 | " \n", 961 | " \n", 962 | " \n", 963 | " \n", 964 | " \n", 965 | " \n", 966 | " \n", 967 | " \n", 968 | " \n", 969 | " \n", 970 | " \n", 971 | " \n", 972 | " \n", 973 | " \n", 974 | " \n", 975 | " \n", 976 | " \n", 977 | " \n", 978 | " \n", 979 | " \n", 980 | " \n", 981 | " \n", 982 | " \n", 983 | " \n", 984 | " \n", 985 | " \n", 986 | " \n", 987 | " \n", 988 | " \n", 989 | " \n", 990 | " \n", 991 | " \n", 992 | " \n", 993 | " \n", 994 | " \n", 995 | " \n", 996 | " \n", 997 | " \n", 998 | " \n", 999 | " \n", 1000 | " \n", 1001 | " \n", 1002 | " \n", 1003 | " \n", 1004 | " \n", 1005 | " \n", 1006 | " \n", 1007 | " \n", 1008 | " \n", 1009 | " \n", 1010 | " \n", 1011 | "
StockACNBABACCATHPINTCKMBNVDANFLXXRX
Stock
ACN0.0003160.0002180.0001530.0001380.0002590.0001670.0000930.0002200.0001980.000196
BA0.0002180.0010200.0002750.0002490.0004660.0003010.0001670.0003950.0003550.000353
BAC0.0001530.0002750.0005030.0001750.0003280.0002110.0001180.0002770.0002490.000248
CAT0.0001380.0002490.0001750.0004130.0002960.0001910.0001060.0002510.0002260.000224
HP0.0002590.0004660.0003280.0002960.0014500.0003590.0001990.0004700.0004230.000421
INTC0.0001670.0003010.0002110.0001910.0003590.0006040.0001290.0003040.0002730.000271
KMB0.0000930.0001670.0001180.0001060.0001990.0001290.0001870.0001690.0001520.000151
NVDA0.0002200.0003950.0002770.0002510.0004700.0003040.0001690.0010390.0003580.000356
NFLX0.0001980.0003550.0002490.0002260.0004230.0002730.0001520.0003580.0008400.000320
XRX0.0001960.0003530.0002480.0002240.0004210.0002710.0001510.0003560.0003200.000830
\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+65h8cee4zu3bszZMgQPvroIzIyMkTEf5ARYlUQVOx2Gw63ZbWBYlXtSV0lEAgEgtZCeHg4o0aN4pVXXmHPnj3YbDbS0tKYPn06Dz74YKPOOXr0aN5++22eeOIJHn74YSZMmMCdd97JG2+8UWOf66+/HpPJxEsvvcQ999xDWFgYAwcO5I477qhzvIcffpi9e/cyYcIETCYT//d//8ekSZMoLi72tHnkkUfQaDQ8+uijZGZmkpKSwo033ggoQWavvfYaTz75JI8++ignn3wyS5Ys4bbbbqO4uJi77rqLnJwc+vXrx48//uiXCUDQdCRZltuVNnC7ARQXFxMZGRnq6RxzZBYeYcKPSjLmRWf9QlJCWr37/rXmG27b9gQxDidLr93SXFMUCASCkGA2m9m3bx9du3YV6YsCMH36dLZv386yZctCPRVBkKjtmm+IXhOWVUFQWbr5VwDSbHYS41Ib1FcjAqwEAoHgmGHWrFmcfvrphIWF8euvv/Lxxx8HTPIvEAixKggqezP/BSDFbkBSqeto7Y9G43JgD/qsmgG7FQr2wKq3wGGFbmOh6xiITAn1zAQCgaBNsGbNGl588UVKS0vp1q0br732WsDgJoFAiFVBUCmszAYJIqWGu2B4iwIEe1ZBxOmE1W/Bb14/rQpJwrjhCySA8U/A6NuhnpVhBAKB4Fjl66+/DvUUBG0EIVYFQaXYXghaiNJEN7iv2p0NgFYq9PJ2wZdXQt4ODmg0/G0y8lJcDAAdbXaOM5u5fslTdFn0GIz8PzjrpRBPWCAQCASCto8Qq4KgUuJUEjdHGxMb3FercYvVVsqv95FVuJuHkhNZY/R3FD+s1XBYG85Ko4HFhzJhzbuw4UvoegpcPBfU2sDnFAgEAoFAUCui3KogqJSoLAAkRNQ/C4Abd55VR2szrMoyLH+FpZkrOCst1U+oOio6UXFoKpa8sQDkaDQM7NqJD6IiwFIC23+GH24J0cQFAkFrpJ0l4REIaiRY13q7sayKcqutg0KVE5BIiene4L7uACsnygVeW3LoFmX9x8iLHueVDsnYJAmHORl7aX9GJ1zMVWN7MKxTDIUVVs6ZfxFq4xEA5sTGsM0UyaWFeYzY+CUkD4BhU8Ag0qkJBMcq7vKjFRUVzV7eUyBoDVRUVAD+pXcbQ7sRq6Lcaugpt1RQplYEZteUfg3ur1EpF7MsSdgdTrSahmUTaBYqC7H/8RhPx8WyW6dDdqo5Lepxpp/dn0Edoz3NYsJ0TO02i0+PXu7Z95tBzW8pSdyfX8CVvz8Mq9+F2/4Ddbv52AkEggagVquJjo4mJycHAJPJ1HoeygWCICLLMhUVFeTk5BAdHY1a3bT7ubhrCoLGnqPbATA4nXTs0LvB/d2WVQCrw4pWE0LLQ2kW/DsXDq7kI72T+ZGRIEtoCi9k9tUnoNNU96C594wBLHvnfnbZfkClrkBtOgDA7JgE4u0Ozig+iJS3A5L6t/CbEQgErYXk5GQAj2AVCNoz0dHRnmu+KQixKgga+49uAyDOIaM1mBrc31es2m020IdIrDod8N6pUHKEQxoNb3ZUcqfa887m3ck3BxSqbr6adilr9p1OXJie9QfzeWHDrWA6yN1JCYwtr+C1zA1IQqwKBMcskiSRkpJCYmIiNpst1NMRCJoNrVbbZIuqGyFWBUHjaP5uAKIcjbs4tSqvT4vNYQ3KnBpFWTbfOYv4JiWJLGMEdtmKw5zCzFFTOaF7XK1dTToNY3srmRAGdoyiS8Lb3PXHi1jClrAkzETuxs9J7Hce6MNb4p0IBIJWilqtDtqNXCBo74hsAIKgUVCWCUC4rKujZWC0Wu+zk80WOrFqzt/DE/GxbDToyZGtyLJEdPkVXD6y4UFjY3qm8ceU2aidykftwNE18NFEsFYEe9oCgUAgELRLhFgVBA2HrRIAldRIsar29gulZfXg0fU4XEEPlYevomL/TTxz1lmE6xu3EBETpiPKoQjdfVotZG2EWT3BUhq0OQsEAoFA0F4RbgCCoCG70/k3MrpVq/XxWQ2hWD2881eQINZi4IJRF5AUqWds74QmnTPC2JMC+y6eio8l0unkzPIyOLgaeo4P0qwFAoFAIGifCMuqIGjIKMl/VY0sl6pR6z2v7fYQBR6U5bKzSAkUc8hp3D6+J5eN7NTk9DLdIr0uBPckxvNCbLRiYRUIBAKBQFArQqwKgoYsN61QqqRSI7mqXdhCJFblzfP5NUzJQmBSjw7aeS/uey6W3NOQnUpAxWdRkWzLXBW08wsEAoFA0F4RYlUQNJwuy6rUSMsqkoQ7NtZiswRnUg1k547v2KvTonJKnJI6NmjnPblHCj9f/RQD7emefZvydgTt/AKBQCAQtFeEWBUEDbdltdFiFdC6LKvllvKgzKne2C1gt7KgUsloYCpP46JhPYM6RK+kCN67ZhTq/BEA7HUUg6UsqGMIBAKBQNDeaLVitaKigs6dO3P33XeHeiqCeiI31bIK6JVTUG4uCcaU6kaWYdET8HQizOrBWpVi0Y3lOPokRwZ9uHC9hmGdhgOwU6eFw2uCPoZAIBAIBO2JVitWn3nmGY4//vhQT0PQADyW1SYEI7nFakVlcTCmVDe7/kBe/jKLTUZ+Uts4rFESZIzsNqrZhhya3A9QxKr86eRmG0cgEAgEgvZAqxSru3btYvv27UycODHUUxE0ANm1hC/JTXEDUPpWWFsmB2nhtu8Z1LUTdyQl8GBiPEWuijInduvbbGOO7tQPSYZitZoctRrKcpttLIFAIBAI2joNFqtLly7l3HPPJTU1FUmS+P7776u1SU9Pp0uXLhgMBkaNGsWaNQ1b6rz77rt57rnnGjo1QYhxEgzLqtLX3BI+q7LM95nLqu122iLpGZ/YbMP2T41DtsYDMDcqAnK2NNtYAoFAIBC0dRpcFKC8vJzBgwdz7bXXcsEFF1Q7/tVXXzFz5kzefvttRo0axZw5c5gwYQI7duwgMVERAEOGDMFut1fr+/vvv7N27Vp69epFr169+OeffxrxlgShpmkBVirAidnaAoFH+XtYoLYCOuSy7pQfvRS1aS+Oyo4kRxmbbVi9Ro1aI+NASWF1T3lu61ziEAgEAoGgFdBgsTpx4sRal+dffvllpk+fzrRp0wB4++23WbBgAR9++CH3338/ABkZGTX2X7VqFV9++SX/+9//KCsrw2azERkZyaOPPhqwvcViwWLxpjkqKWmhwBxBNYKRDUCHS6zaK4I0q5rZc2AJO/Q61DIUHbmSASnJbD4SybjeCRi06rpP0ATCKs+kJHweAIdLDtKpWUcTCAQCgaDtEtRyq1arlX///ZcHHnjAs0+lUjF+/HhWrlxZr3M899xzHheAuXPnsnnz5hqFqrv9E0880bSJC4JCk/OsAlrUgB2rvfndAHaVHwWgs0VF+rXjGN0jHrPNgV7T/HbOe068imf++wKz2snW/ANCrAoEAoFAUANBvSvn5eXhcDhISkry25+UlERWVlYwh/LwwAMPUFxc7Pk5dOhQs4wjqBtPgFUTfFa1rrIAVoc5KHOqjaNFiiDWyhKjusYCYNCqm1xatT5MGtqRJJsJgNyyvGYfTyAQCASCtkpQLavBZurUqXW20ev16PX6OtsJWoIgFAVAC4Dd2fxitdJmBUAjqdCoW95r1CgZgDLKLEUtPrZAIBAIBG2FoN6h4+PjUavVZGdn++3Pzs4mOTk5mENVIz09nX79+jFixIhmHUdQM8FwA1BJimXV4awegFcfbA4b325bQkFlUZ1tnU6HMmajRgoCKsWa6yg/oBQnEAgEdSKLz4pAcMwR1Pu0TqfjuOOOY/HixZ59TqeTxYsXc8IJJwRzqGrMmDGDrVu3snbt2mYdR1ALbjeAJlxWaldfh9xAsSrLYLdw03cP89iaWzn/68vq7GJ3jdEUcd0ULPrOAFgdZZC3KyRzEAjaEvvzyjn+ucW88sfOUE9FIBC0IA12AygrK2P37t2e7X379pGRkUFsbCydOnVi5syZTJkyheHDhzNy5EjmzJlDeXm5JzuAoP3isaw2wedTLSmXZEMtq7e+PYwlJm+fIo7U2cdrWQ2NWDXqYsAMRWoV7PodEnqFZB4CQVvh1k+WMUL3Isv/OY47T38p1NMRCAQtRIPF6rp16xg3bpxne+bMmQBMmTKFuXPncumll5Kbm8ujjz5KVlYWQ4YMYeHChdWCroJNeno66enpOByOZh1HUDNyENwA1JJiWXXK9f8/lpTl+wlVNxa7Fb1GV2M/J26xGhpHgHhDRzDDVp0ODjescIZAcCySwnP8nVBAZOwCQIhVgeBYocFidezYsXX6DN1yyy3ccsstjZ5UY5gxYwYzZsygpKSEqKioFh1b4KbpllWNy7Jqp/6W1R0HNgTcn11eRKeomitROUJsWe0VOYilRbBTp6XEUkJkSGYhELQdNkfnASpK1M2bB1kgELQuROEcQdAIRlEAjxtAAyyr+7K85UovKLOhcT1M7S84WGu/UIvVEWmdMVjDkCWJDLsoZiEQ1ERWsZkpH66hNARZOwQCQegRn3xB0PC6ATQhwEqliFX3En19yCzcC8AAs4Yn/i8Du8uyO2PpNO78604mfDOBx/95nIMl/uJVJrRidUDHKCIqEwBY5ygNyRwEgrbA7xl7iMycE+ppCASCENFuxKpIXRV6gpG6yu0G4HRZaetDrqsSVTRG0Pjn3F10cBGZ5ZnM3zWf+5c94HfMbb0Nlc9qpEFLgpQKwB7ZGpI5CARtgeyD77AkbZNn2+gU6asEgmOJdiNWReqq0OO+faiakg3AZVl1NMCyWmhTKkDFaGJAkngyt4Tx5RVoyv2LmG7L2+e37QyC20JTSYyIAcDsFIGBAkFNFFj8KxNGOYRYFQiOJdqNWBW0BpruBqBRKRWsnNTfslrsVJbQ4wxKMNUkh8QrOXncZjgDy8EpJO1Vcq7aKcXq8Fow3WI1VJZVgKSoaACsDXi/AsGxRoXLTWaAVfl+cITu+VIgEISAVl1uVdC2cAdY0ZRsAC6xam+AeCuSlNKsyRFpyvCVhQBMO/go0wBZgmFyGnZJIrs8m7RIpZ1DdoAEKil0YtWkDwfAJglLkUBQE5VyBQCRkgkobkCuEIFA0B5oN5ZV4bMaetzyskmpqxphWS1QK0voHeN7umfgd1wCelhtAKzKXO/Z7x4jVAFWAHqdIlatCLEqEATC4ZQpl5UH0hitkuDNKSyrAsExRbsRq8JnNfS4swE0ZVldq1aS+DtcQnJ/Xjld7l9Al/sXkP7Xbiqs/jaVw0X5lKmVO1evtMHKzmm/wriHYNCloDFAn3MYZVZudk+ufpihnw5lzdE1Pj6rofsY6PXKzdcmAU7hCiAQVGXdvnyKNBYAOkd3BMAuxKpAcEzRbsSqoDUQhGwAardlVTnX3H/2ERW1jKSkL5i95E/um7/Jr/3f25YCkGK3k5LsKlfa+QQYcy9c8C48nA3nvEIfi9dX1e608+SqJz1iVR1CNwCjKRYAs0qCQ6tDNg+BoDUiyzJf/v4lB3UqVLLM4G5nAeBwHRMIBMcGwmdVEDQ8eVabIP50GrdlVTmX1vwDztQFVABhsRs4WHAi8I6n/YFMxZLewaYGrSHwSY2xHGe2oJFlTw7WMrMTp9z0gLCmYjK43AAkCQ6sgNKj0HsiaI0hm5NA0Fr4fUsWZfYPARiuiiTe5ZdulyQcThmNWphYBYJjAWFZFQQNOQjlVqMMJkAJsHI6ZY6WLPQ77tCu8tuuqMwEwCiZaj6pWkOsJoo/Dh0h4cipAOQUy16f1RBaViO0yrwtkgR/PgXfTIMlz4VsPgJBa2LT+nRWRSr+5rce/yB6rfIwa5ckbHaR7k0gOFZoN2JVBFiFHveiXFMslTFhinhzSDIfr9yPzhHrd7xc5WB3jrfaU7lNeW2SarCqukkdSrzDSX+HkilAq6v0pq4KoVgN0yvztvgK/M3fhmg2AkHr4oh5DQCjHfEM6XUOep33odRqF4U0BIJjhXYjVkWAVehx+5k2pShAvFFJkl+klnnip63kVCgpa0ZaFYtKnlbi7m/+BeBocTl/sRuAMHXty+b6jkMBuMxVJ8CpzudPbZYy3xB+DCJ0yrztkoTNvTMsIWTzEQhaE+WOMgA66FMAMOi8n3OzpTwkcxIIBC1PuxGrglaAJ+Ch8ZdVx/j+ABRrJJAsIClLfbGqSE+bcj6l0l7J1T/ejkOlWEetkrb2E8d0BmD4/h+qHQqlWI03RaFxKK7jO3WKICcyNWTzEQhaC/llFsxyCQAxJuUBTucjVi1Wc0jmJRAIWh4hVgVBQ5bcltXGX1aRif2JcCgCNKLPY2yIyQXAqI7wtCk0rGXMZxeS7fRGzw+L6lj7iaMVk6pehuMrK/0OqSR1o+fbVEx6LbZKRUhv17kEt7CsCgRs3rGJ/8KUVZq0OCWwSqvxuvtY7JUB+wkEgvaHEKuCoOE2rDYlwErSmUgOkG5Uq9Lzc+QoACxqmUpJqRU+qtLMs7l5XBjTqfYTpx0Pvc8G4NncfE4t9l76ZVLo6uFE6DUgRwGwv+s4Vhv02GziJiwQ7Dz4u+d13x6nAKBR6z37NuzP5pzXl/HP7rwWn5tAIGhZhFgVBA1PUYAmBiwlU93/VK/W0/mc1xlZafHsC3M6eS8rh3PLKlB1GV37SbUGuPxzuHI+CQ4nT+TneA6VSKEL1JAkiQid4uIwt2wj16ck8aF5f8jmIxC0BmRZ5mDeXs92r9SRAEgqFWrXU/GLCzex+UgJV7wv8hMLBO2ddiNWRTaA0CNLTfdZBegSYBlcrzaA1shodaJn38hKM9JFH8F1f0DXU+p3clcAV7RU4dlV7KyoqXWLEKWP8Nv+2pZTQ0uB4NjgiflrqSjdDMCpcrzfMY3ra0arLcQQ/xuSpqiFZycQCFqadiNWRTaA0OORqk20rHaI6FBtX1KEIugm9jrLsy9CnwADLoC0kVBf1wNjdLVdcaHzAgAgXBvptx1H6HxoBYJQY3c4Cd83gz9jlUwAg9KO8ztucn3RlHT5Cm3CX6SkvdnSUxQIBC1MuxGrgtAjy00vtwpgCk+pti82TBGrKX0vYFx5BTEOB9Ojejf85NFe39Z5mVlcWFrGFcX6Wjo0P5E6f7EaU1EEzroTntsdTgrLRa5JQfsityCfb+IrsEsSp8X05/JTnvQ7Hu70/34pNZS05PQEAkEIEGJVEDSCkQ0A4PT+V9HTamNSaRlnlJWTZLczMFKJmCehN686Y1h08Ahdhk5p+MnVWhh8Bai0DLJYeTyvgBRLcZPm21R6RAxBdnorH3e022Hf0jr7vfjbDoY9/QevLtrF2a8t49zXl7M/T+SeFLRt9h38j3KVCpUs89I5n2LS+leni1XpqvUpt4R4eUQgEDQrQqwKgoanglUTxWp4Qm++vexvnhp6J7Nz81l0KJOU2N7ukyNdOR/dtIXQ/dTGDXDuqzBzG9+ozgRgc6+bmzTfptIhIpWK/TcRX6FkBXACWMtq7SPLMu8u3YsswyuLdrIls4RNR4r5Ys3B5p+wQNCM7D2kFP2Id0hoVdXzJ5dSfTVhn3hIEwjaNZq6mwgE9cOdDaCpYhWAiCQYfRv0PQfydkOnE73HYjp7kvw3Co0OwhM4+fa5/LB5J2eNHND0+TaBrvFhOC0dKCsbBKZlWCUJymoPsjpY4A0KU+lyMHZ+D5WmlAOlzwF9m3nGAkFwKTXbeGHhdhLCDeQf/QtMkChFBmzbxZjIXqv38xHpcHLjZ//ywZQR9E6OCNhHIBC0bYRlVRA0vJbVpvms+hHbDXqdAargX6pJUUbOHz0Ygza0AU0n94zn/ol9KHIqllWbJEFpVo3tDxVUcOX7q5F0uegTf8bU9XVUmlIA/sr8CbOtbn9XgaCxyNZynLm7g3rO79ftZtfmN/lo+Q/slrMBOC56WMC2t495jitjBvFRv5sAsEgShwsrmPhq3a4zAoGgbSLEqiBoePKsisuqQUiSxI1junNaH6XMqk2SoHBfwLb78so5dfYSDhdWEtb1dXRxy5FUNs9xTfgObvl8vSfYTSAINpvfuJDct0ey9adXg3bOw3tfZ2vachxdP2ZTmFIVpE9i4BWPbqkjuf+8eQzuPgG1LGNRSag1RTjFJS8QtFvajaoQeVZDj7vwlKoZrKDHAjq1EjhilSTY9D/I31Otzcf/7MfmkAE7kqq6755Kl8+SIwvZfERESAuCj1xylPcNuxjfqQP/7PogaOettB+otm9Qt9q/y7XRXehgV1YREvR7a20rEAjaNu1GVYg8q6HF5rCxXe+KyA2Gz+oxiF6tBJMsNbkqeL3uvwxaZrHzv3X70Sf9QIdBL/sdC9dEe14bO3zN/iJRWEAQfA6+fxF/hinR+d9FBPFzrvJG/A9yqLgyog9pKUPr6KOmE8pnpqLjt6j0R4I3H4FA0KoQqkIQFDZmbfS8diDW4xqDXuPN97pF54qC9lnOX3+gEItuC7rYlZTYCvz6ltmL/LYPlm1vtnkKjlGcTt7TZXo2I+Xg+Xq73VZOssQw79oN3H/B/+rl+95Vr1Skk1UOwrq97vHXNtsc5JSYgzY/gUAQWoRYFQSF/AM7Pa9zrUWhm0gbRq3yCtOjGleiDrv3hrsrpwxt9LqAfZ8e/TRdIrt4th3CgU8QZIr3ruWHiHDPtkZ21tK6YXgziTQsOLNLmH+1O4tNmdOFb/3DyGcXi5RWAkE7QYhVQVCwSV5xZKksqKWloCbMziLPa088v6XUs2/NwYOow3cAcFXfqxgYP5CFFy7kk4mfcF738/hp8k/oHV0AcAZRSAgEAPsW3Oa3bSd4WSc8YrWB/TrH9PDbzi41M/7lv9mSqfhs/5iRGaibQCBoY4g8q4KgUOkjqibrmpAD9RgmXp/meV2sdi2xmksgPJGD+RUs2p2BqbOTZGMa9428z9O2Q7jXuuQudWsXYlUQRIp2LONFQyHgdVWxSkG0rDayVHPXxCFw8DvP9tdrD7E7x1tQo8IqKlsJBO0BYVkVBIUKi3KD6Guxctw599XRWhCIONVAZKfiq1rkzqjgKgU7Z9FOJK3yukt0h4D9ASTXR1qWRa5VQfDY/NN1bDIoQrW/RrFm+q6mBMLZIFcUT5bmBs0rMdU/CFGn8e9fLsSqQNAuEGJVEBTMNsU3TEaHJjIxxLNpmxh1GqwFowE4ZIxVdpqV5cwKqwOVRnmdZEqq+SQunz+HyLMqCAa2SuTKQm5K8FpUJ6adBoClFrH61pI9DH7yd3ZkldbYxhevG0ADHQGiO/PDYe9Sv1ylFGuFRTy0CQTtASFWBUHBbFXKf2plcUk1lslDO+C0xgHwvdHJvQlxUJ4LgM3hRKVT0lH5LvtXxW1ZFT6rgiZjt3Dk9ePY+7LXL3Rcwjl0juoCgK0WXbnp97m873yUN36oX1WpRotVSSLtyh88m+WVygOdOnwruoRfxedAIGgnCGUhCAoWu0usEtrSpW0Zg1bNhF7eqj2/hod5yq6WWIvQRv8HwID4wJV9wHuzdzrFTVrQNDL3ruLiaIlJHVM9+54//REiTUq6KLMkIddwnb2sf51owx4uL3q7XmM1NsAKQNNhBCrXSkKZuQB98nxMaZ+gj/+bLMfKRpxRIBC0NoRYFQQFi70SEGK1qfSK7eK37TzwDwAHVZ949g1KGFRjf7dYdQiLkqCJ3L/kMUrV3lvEyNizMGlNhIcpYtUiSVitgXOZ3pCcwEUdUtiiy6vXWN7ywA2Xq5JGh87Vf595GboYb2GYSvt+5v97mB8yRMEAgaAt026yAaSnp5Oeno7DIXyUQoHFody0tFK7uaRCwuDUTnDQu23e/QemykIqNN6iC1H6qBr7S5JwAxAEh0xVHiAxTu7J/Re9QUpYCgDREYqrilUlUV5egt5gqtZ3vcEAwOIwC9MbMGZD86y6OqGTwQxkFuZAnPdQbOVW7vrfBgBO7ZNIhEHb8PMLBIKQ024sq6LcamixOSwAaNvP809I6JUU4bddiRO2/4LG0geASV2m1Nrf4wZQk1iVZRAuAoI6KCjJJ8f1Ub5j3GOkhqd6hGS4wXuNlpYX+vUrqrBSafUaDOpIGOChKW4AAFrXOAPxt6DanZWe10eLRUUrgaCtIpSFIChYnGZQg0ESloumkBJl8NuuUEnEbf0BO8rDQM/onrX294rVACrBboG3RoMpDq5d6MkcIBBUZe22P5EliUiHk65pA/2OGTTea7S03FsApLjCxujn/yTaqAWXm6vdAXaHE426DrtII/OsunHnKqiaTssue7MDHCmsrPYwKBAI2gbtxrIqCC0Wp2K10KkMdbQU1IYkSdgruni2KyQV7PkTGcVCFG2s4WZrq4Qt36FyWagCWlYL9kL+Lji0Cvb9HeypC9oROw6vBiDVrkFS+d8mVJIKrSuHaml5kWf/4dwCvuFuHq54zrPPgK1eFk2nO89qIx+gdK5+ZpV/f5VPvuGsEmFZFQjaKkKsCoKC1WXB0Aux2mTMh6/0vK5USeC0IamUG22sIdK/sdMJy2bDM8nwv6kk2JX0VtXEank+FB7wbv/7cbPMXdA+OFK8E4BEAvtH613assJc5Nkn5e8iJyyXzsYMz75IqZIdh7KZ+VUGhwsrahmx8QFWAHr3ioLK5rdfkp0YUr5Cn/INpWZboK4CgaANINwABEHBLVYN6urBFoKG8eolp3D/qg9QG7Io1YWBxYqkMiMDMVUtq/uWwOInPZux9nzQVskGUJoNrw4Cu49lyZW/VSAIRI4tG9SQbEwLeFwnK+KwwlW0AiC7/Ci3JfsXBFlqMrLhyyUclhPZm1fO9zNGBzyf22dV1UixqkMFOJElf0FaonGgNSkp33LK84HujTq/QCAILcKyKggKVpSbhEEjxGpTOW9wKhrCACjQKr9RKQ8D0cZwb8PM/+Cb6/z6WlRKez/Lat5Of6EKYClBIKiJfJViBe0U0yfgcbdYrbR4K1QVmfMDtj1L+xeb9dcSdeRvvlhz0CdNlS9Nq7imc4ncI8ZKv/02lfe8+ZXiAU0gaKsIsSoIClZJ8Q0zaMPraCmoD3qVstxfoNVjB2SV8vcN04R5G707FioL/PrF27OBKmK10idiu/8Fym9L/cpgCo5N3IFKJkNMwONucVhp9V5HZmt5wLZX6xYQLpl5UfsOD3y7iVV7C6q1cQvYRqWuAnSSkt+5TOOfutDh4xZQWb63UecWCAShR4hVQVAwu8SqUSeibYOBSa2I1XyVhnKfoJEwt6W1qnXKtV+SAwRYucVqrzPhlHuU12ZhWRXUjN31W6fRBTyulRVxaLFVcO+85Ux7YwGV1sA+qUtNBi5OTSZXX8EZqrUBfVe9V3PjxOpJUljA/WYfy6rF2kYLAxxeB7N6w5r3Qj0TgSBkCLEqaDJ2p51stSKOkk2pdbQW1IcwrSJWC5GVjACARgat2pUarKCKlSgiGfB+oDU2HzHqFquGaNC7HiaEZVVQCw6XZVWnDRwwqXNVqiuuKOHhnRfxRu40CksCV6t6Pi6W7Xod9yTG8a7uFQw5G6q18eZZbZxYvUaTyDllXsuu0ZVL2FeslpZl43RlMSgst7LxcFHDB5Jl+GGGn594cyO/Px7KsuCXu1tsTIGgtSHEqqDJ7Cs6gFWl3CD6JAf2cRM0jHi9Ui1oZVgi5a7UQQan60a+/Rd4fZh/B73ifuG+1RvN2d5j7ohtY7TyA+CwQElm0OctaB+4F9O16sBi1V1WuTx/L19Fa5kdb0Jtzqr1nIe1WhaEmfhj+fLqB5voBsDwaWh8VhuMrteVPqsSTnsR6w8qD26T3lzBeW+s4N8D/kUN6iR7C/z3mZKBw2Gvu31TcdiRmujPeyxTYi1h6sJpfLHti1BPRdBEhFgVNJlVe1YB0MNqI7XH0BDPpn3QN+p4AHIdBzlkVPwG9bIacnfAl5f7N+5zDvSfDHg/0E58fPfcy7O6cMWy2tkVkf3v3GaavaCtY3dpPL1OH/C41lX8I07az2ux0fwvMoKjcpHn+JR+U+hnTKnW7/7EeGzGbPLLLAHP21jLKn3Opswnc4ExQJphSWXmgW83kVdm4UC+8plYvC27esPasJZhwyXmfdJ2NRe2o5s9ry2yprr7j6BGisxFXP/rDfybvY5n1zwb6ukImkirFKtdunRh0KBBDBkyhHHjxoV6OoI62HxoJQAdbRpUxsg6WgvqQ7+kVJxWRaT+ljIKAJMMbPjSv2HPM+CyeTDqJuhyMu5VT9nXZ9WdCUDjEh4jXdXa133UMtYhQZvDLVa1GmPA41oUsbrTx/BqcxUGGWI1cfeIu0lJ6Oc5NrbjWM/rs4wLq1k0nVLT3AAA1C5rL0DAnCQqK7tyyhj+9CJABslKmL5h2Rut5Xmc1zGFqSlJUFE9UCzYHP37Q5zAIpORUrWTt37LYOZXGdgdomRybWTkZDDmq1PYVrS57saCNkGrzbP6zz//EB4uIsvbAgdLdoEESar4UE+l3TCsUwyOZamodIVscSX6D5edsOl//g2HX6v81hpg6s+UvX0cYAWnj2XV4So5qXYFy/Q5B7QmKM+Bwn0QX3sJV8Gxh8MlGvXawGJV57CDFpaEeWVhqV1JG6VTKbeVK/sqxS1uGHQDfeP6cuFHQ9ipclApqdizez9n9E/2ntBTbrXxqCWv7cUYwA4jq72lVw0p36CJ3Ehm2StAj3qPserIBg5rtRzWgqUsC31CrybMuA7ydtFp18e8HBPNR9GRnFFWzsolG8kknnMHpzKgQxTx4brGu060Mz7e8B6vZLyBAyHk2yOt0rIqaFvkOJTAitSw+n/pC2qnY4wRvaMTAPudRwGItZdD8SFlOX/mdpi2UInw90FyfaRlXzcAu2vJ1W1ZVWshpqvyumBf870JQath2YH1rD68qd7tbS79Y9AH9lk12qsv47urR8UYlD4jkkcwZ9wc+sb1BaCPQXmY3arTUbxvXZXe7nKrjb8lqXz6SgHOExvp3iejjf4XSWVjQ9EfDRqjsNibqzW74GCj5llfDiz/HICPopXVqt/Dw4iRyjBi5p5vNjLimUXM/Wd/s86hLTH3v+pC1R1018Uqqpe1dRr8zbB06VLOPfdcUlNTkSSJ77//vlqb9PR0unTpgsFgYNSoUaxZs6ZBY0iSxJgxYxgxYgTz5s1r6BQFLYwVxWIRl9Q/xDNpP0iSRJfI3n77TG5/tZiuEJkCnU+oVkvdvYwqB7Cs2tQarvnlGm5edDPOmC7KscL9zTF9QStBlmVe+Oddbl4yhev/uB6rve6btt1uxynVblk1q6tbr/RqJcNEmCk2YJ9KlxVyfmQ4xvxNmG3ea9RbbLXxVkJfsToAPSdW+BcI2CoXg2RDG+sN8CouV9MQCixe94X87G2NnGn9yM7OpsQnQEzvdLJA/yAr9LdhLlPm8fnq5hXMbQWn7KTQ9YB+SkUlp1RUcmtBEQMZCIBFarWLyIJ60mCxWl5ezuDBg0lPTw94/KuvvmLmzJk89thjrF+/nsGDBzNhwgRycnI8bYYMGcKAAQOq/WRmKtHJy5cv599//+XHH3/k2WefZePGjY18e4KWwJ3mxhjbOcQzaV8MTerntx3mSseDKXCidsDHMlXdZ3WPrYz/cv9j2ZFl/GFwWVkLhWW1vbJ492ae+Ps9Ptv1urJDVcHB4sDppXyp9Enur9fXkL80tXe1fVkaRfjpkgYG7FPhc00maHey+UixZ9sjVpuwpC07vbczg6Tinexc9E7vmKU40ERsQaXxpnWLq9yC/M4Y+K9+RpFcn8pvJXnbGz3Xnzd9zLzVs2uo5qUgW8tZa/Batt3vLlYqY6RqO72lg/SMD2z5PpaQZZmc4oM4XNfOxoP3cfTgDOzRTxLeb6LSJpQTFASFBj9uTJw4kYkTJ9Z4/OWXX2b69OlMmzYNgLfffpsFCxbw4Ycfcv/99wOQkZFR6xgdOnQAICUlhbPOOov169czaNCggG0tFgsWi3dJqqREJDtvaWyeyGHhYxxMhnXsyHwfbRHmyhGJKa7GPu7a6k7Za7Wy2s3clRjPxr1fefZ9UZbDBBBuAO2UVQd3cfvyK5EkfwvowYIj9IirHqXvS6XZm7TfqAtsWa0IYFndpVN8ooenjgrY557h97DiyAoAVIZM9uWVM7yLYoWVXUK2Kd6XsuztrXVV0otyOslR+fiydvAPUBylXod0tBB+uBmGXlnnGBWOSo9qrChpXEWs91c+z6s7FXHcO2EAw7tNCDyWrYQ7khI825UqFRWShEmWeVH7LvFSCV8VXA8E/nsfK3y6/nVe2qwUTAhzOnnw8rMx6DWM653Iz0vfB8Ap3HrbPEH1WbVarfz777+MHz/eO4BKxfjx41m5cmW9zlFeXk5pqbKcVFZWxp9//kn//jUvLz/33HNERUV5ftLS0mpsK2ge3GLVpA8YgytoJIM7xiLLPhHO7gh/Y+BlVvDxWfURqxsdZSwJM1Hg8FrMstxLsKVHgzhjQWth6f6MakIVICu3boFlsXqXzw01WFav6XcNAKd3Pp1eVu812ludwBmdzwjYp3t0d67seZEyD4MZybcMsMf01fhbkq9lVR0WD5d8wmCLtZYe8K9Bz+QOyXwTEfh9AorP97f/Bxu/xuzKeACQa82FVwZAVsMizjO2e4Mkl+3+qcZ2qzXVreC/hZlYEGYiXlKMMmNKFzRo7PaIW6gCJNtkTu+fzLjeiQBoVErWChFy1fYJqljNy8vD4XCQlJTktz8pKYmsrNoTRrvJzs7mpJNOYvDgwRx//PFcc801jBgxosb2DzzwAMXFxZ6fQ4cONek9CBqGw+nwLL8YDMKyGkxSoozg1Hq2PW4AEck19MDrw+qTuirTaq7WzCy7UlY5ar+ZC9om+4sDlxbNK67bx9G3bKqhBsvqhC4T+P7873nh5BfQ+txGRob1rHUpv1+Skod5q15HZNEWz353BStVE9wAkiK9c9WrtNDvfB4uNnN+aRmfZmZxfmlZtT479Dp263Q8EV/zagX/fgwbv4Jvp6OyewX2Lp1WCXj84eYGzbPQ7v37bincBcDOrP+Ytz4du1P5XNocTnJlpZ1e0pBiVMTXowlx3J8Yzw6dlu06LVvU3Rs0drBwOmW+XnuI+f8e5uHvNwXVd3bLsh9Y88XTyM66JWZlkf/9flh+fzRqn4cWV2YK4QbQ9ml1XsfdunVjw4bq5fhqQq/Xo9cHTlwtaH7Mdq/YMelqsU4IGoxOowIf61iU+8s7obq/oBtvNgBvv1xZcZOJKunE0cIzMHV+HwuuQBt7dSEraPscLQ9sMS8qq9uSbrEpIkkjy6jUgW8RkiTRPVoRShofsRpliK713P1iFT/sbTod2nLfCmreEKvGEmkwgOtyDndV3orVRfB0nvKeU+zF/BDRiAdqn9UHyVkCKOfYqtOxXaelt62yQbMu8PmbbqvIpqw8hwt/UyzVz296m18u+IWv/qnALinC9bKE0ax3FnC00hv3cV1yIsVqNZMKCzmt4e+oySzdlcu98/1jSS4fmdbkNFq/f/EqZ+x4FICNCxMYdNYNtbbfsutHz+uLd4/gv+Rr/Y6rXC4gwrLa9gmqZTU+Ph61Wk12tn9VkOzsbJKTa7EGBYH09HT69etXqxVWEHzKzd6lZZMxIoQzaZ9IKu/DQAebyxqaUHNJWz83gENrYMdCCmRFmKaExfHUeccBYJPcYlVYVtsjlRW7/baT7cq1U2bOCdTcD6vLEq+pZ7UkjU8y/sTI2nMtd43qil6WqFSpyHLke/YHIxuARvLOI0LjCjxyp2gDIuphqQtEsbmQCzokMycmih99xO5OvY6LO6TwdQNtJYWS9+9aonKw8L93/Y4//tttvLlkN3aV8j+LC4smwZjg16ZYrbzXvyPKCQX/7Mmvti+rpGkPvvsPHuSMHY+yQa/jx/AwBq25l8IPLgBLac19CvYA0KtCy4e2C3lmsn9si0btcgMQPqttnqCKVZ1Ox3HHHcfixYs9+5xOJ4sXL+aEE04I5lDVmDFjBlu3bmXt2rXNOo7An/JK7xeJSbgBNCsd7C4/09huNbbx5JeUHfDZhfDFpVicyg0tXBtBjOt/ZBeW1XbL/I3/cUS9029fZ9eDTpm1MFAXPyqtynK5tp5rp75iNT4isda2apWaznYlEOuIn1h1FQVogmVO5RNIFa6PUl5MehM6joRLP8M4cZZf+2iHw287YCnT5a/wv13z2aXT8UF0VMBxP9HU/4HPUllIuSsdVZLrAeKz/f5+p0XFezmO7ahUyopIWEQHxnceTyB6mJs/Vfq+vHJW7Pb3n92ZXV1A7s6p7mZRX2RZ5n9z53BIo+Gq1GQeSohjsclIzKHF7F/xv4B99q56jU0H/gJAa9ez+K4x9Ev1r6CodqWsEpbVtk+Dr/SysjIyMjI8Ef379u0jIyODgwcVn5WZM2fy3nvv8fHHH7Nt2zZuuukmysvLPdkBBO2LctdTr84po9WJNCrNSZrr5oZaW0sr5SMtOW3gSrNT6RIAOn0ssSZFrDrUZv40Gb0FAwTtAqfTyeNrb6m2P9Wu3LQrnDVbqdz8sVcJho1y1k+tRup1ntcREal1tg9zeZ9ZnNWvvSaVW/XJpRlucAUhxnaF6/+Avuci9TvfY2GOdTiYVFrFKmmroBp/Pev5/NSE01HrYT8KCxWLt0aWGVCpSKg9Dn+R53DY+Ez3HJUuUWuKSuOcbucwZ+wcbh92u39byeGXrzbY2BxOrnxvFVe+v5qfNnjdNjLzS7hP8wWfnlzAoI6KiG+KWF22K48R8mIu7uBdgX02LoZvIsIozKkeh2Iuy+H8He/xrV75f0ZrI+ieUN1YonZZoIVYbfs02Gd13bp1jBs3zrM9c+ZMAKZMmcLcuXO59NJLyc3N5dFHHyUrK4shQ4awcOHCakFXwSY9PZ309HQcVZ+WBc1KpVn5gtIhI9UqogRNIc06EGnASBh9e63t3JZVley19mwNGwzsQWNMJMHktQ7dkRjP0sPZRDfHhAUh4VBxIfjkEXWTpI4BSqmQK6t3qkJx4QZQw0Bb/bJ7xOoM4NKdkdE1W/3dqF0PVA53kB/BsaxqVN7bWYQxQMCUMZon8grYpdVySWkZS0z+wWNy3i6k1CHKxpLnwWHD7LSxTV/doqqWJU9+6YZE7xQU7Qcg0gFaaxRQ/f8hSQ70ko1iteJfEKYJQ5IkTuuseKeOSxvHPxs/5sV93+GQZHJLLaTFBj8TS3aJmVHPeldJb/3iPxIj9OSXW+leuJybtD/B2p8YqO/AN5pB/LbldqaN7lrLGWsmN+swfyaWU67y/k9yNBqeiI/j2dzfGbqhDwy4wPOgXmYu8OtvUAe2ersDrIQbQNunwZbVsWPHIstytZ+5c+d62txyyy0cOHAAi8XC6tWrGTWq+fPACTeA0FBhUawTWlmuVk1J0HQu7/gimrITee6sWXDRB5ASON+wF+V/oHK6xKo2jHyVEvgWpjXSIToWZ9HJAMiSRLYkQ0N9+cwlUF7dZ00Qej7bsAgAlQyDzV7LZXKYktKvXKrbkm53uAKsdPWLMzCFe9slh9eewxW8YtVqC7R83hQ3AB/LqimA76xay4mVZqaUlGKUZQwJx/kdrtz5i/Ki8AAseQ6WzeL1mCiWVRG1p3Ycw0BHR++4DZhzoSsbQ5hDhaQK7DJhR+LXMBPbdcp5w6vkr+4e3Z1Elw+rXXKSW9Y8qyNfrqlu0bz03VXcPG89vVS7eSg+lu/Cw1BbM5mi+ZW1e3PZl9c4H9qs/YtYaqohp691P3z3f/BUPOxdAoC9yrUjRY4M2Fekrmo/NL/DyzFCbZVI2jNmq1ushngi7ZQHT5vIfzPeYXCHupdXAVSuIBOVKwMAOhM2WfFLDdeFodeo+eLCZ5HNys0uX60GRwNudk4HfHAGvD4MynLrbi9oUb488DSgWJJ8l6/jY5TI/XJV3StPNldAnk5Vv8ghOdFbac2krdvCp3Zdo5nF5Vhcftgey2oTxKrVJ11bhKkG39meZ4A+Ci6dh6nbuX6HDu7+XXlRuI9MjZp9Gi2fREVWO8Wdw+/GKOl89tS/ZGtBmbKUbnBoifWZY2eL9wt0v07LvYmK2O4Z05MhCUOqnUfv+jvbJJlDBQHcF4JAUaUiCG9U/8hb2lcw4fVvt8ce4seIcB5NiOPELmnckJzIYGkPn6060OBx/tqRw6rMDAB62XXMPG4mc8bN8Rzfqfb5H3xyPuxaRFGl9z1XHLqGrvH+/0s3Go1IXdVeEGI1CNz6xX+cOWdZs/oOtVZKLUrJRCFWWwkuN4BIqyvqWxfmEasRrqINgzpGE65VSrbmq1UNC7La8xfkbgNzEez6rd7dnLKwbTQ3Nrt3Wb2fxeLxeQSIS1IKq5SoFb/WWs/jCr7TaQJbuqpybs9JAAxOGFyv9m5BKkkOckqUB6VguAFU+hTCMIYlBG50+Vdw907oew4xfSb5HVpRugfK88nL2s5FqSmclxbYSpwWkUZlmDd9nFqu/5zzypRMORqHnoTIDp790dbqIr8HRj4/63O0Adyr9FpltcSmgoxDRfUevz6UmG08+sNmPlqxHxNm7td+yUT1Wq5S/8HGx8/g4bP7Uhrl/52x1mjgK/3jfLx8F5sOF9dwZn/MNgcXv/0P13+yiIwUJQ1WlEPPtAHTOK3TaVzZS0lb9XW0hnM6prBXqwjP8v++xu5QrlG9U8ZR1o8u8dUfKsDrBuAQi35tnnYjVkOVukqWZX7d9zP7HN+xau+xtzS6Okf5kulpFWKkNeD2WfX4aMlOHC4ra6RPHtxwjXIzP6TVNix91X+fel/Xs1TrjoLdjPjsRJ5Z8Wr9xxE0mH1F3pSBHx3NoULyfr2ndXKnLJM4VFB7gRYbiujVq+snVnvF9OLXC37lvTPeq7sxILmrsklOnK4VKU/qKqnxt6RKl0UYQDIE9mFEpQKtEggaHxbjd2i50QA/3EzG4Q2UqgPP48bBN6JWqdHpvVXkNPWx2zmdcORf8sqVh0jJbiLFJ62WZKsutiK0BgyawEGrBpcfrU2S+WtbdpOCm6qycHMWn6w8QAKFbDV485Ze29tGpEHL9Sd3I9dZ/QH3qEbDWNUGNh2pn1gd+PhvrN1fQK/Udzz7dJL3/Z7X61Si9dEgwQGtlvM7pvJ+VCTF+//1iFU1Mq9dPpSJAwK7rGjUIhtAe6HdiNVQ+ayWWuwYO3yNPmERu4u3tejYrYEjpUpKk66OdnMptWlUeJNgl0sSd2nLsasVERNl8IrVgfGKFew/g656FHRFAfw8E3b9oWwXH1H2/XQ7bP3e285SPZAnEDcuvAers5wvd7/fqPckqB97CpTk9XF2ByZZ9rOsRiX2Ism15L58/TcB++fm7uPQgQ3YUNrpNPUP2ukY0RFjPS2xuC2RkoNKz2pU090AKu0+wUr6unM+Rxr844vXGQ2U7P6NnDxvsnvZqaWf6Xx+PW81G6/ZyIwhMwAwab0uEvUSq2vegfdOpaxIqVilJpIkH/9ztRTD2WX+/p7bnTUHw+nDFRcCqwR5BfmMf/lvNh0uZvORYgrKm5Y7ObdUebh9KGEZAI/Hx3JehxQ48B3sXw5OJ/k+DwZuzk5L5T3dbMpKCqod+23LUaZ+/DfrDyqp0/49UIhTt4+Ivg9wONzbXtJ7594vrh9LL13KtP5ewfxqbDS/aYqxuR6wVTKcNzgVbQ0PF+48q7IkHbOueu0FoTCaSFaJ94a9dtcmrPZj6xlOZ1O+aGSNKAjQOnClapEkLuqQzO/hikB12sMYlNTT06pXXCcAilRqqKxyc5l/Paz7ABbMVITqGyPgxa7w71z/dub6idU8S/BKMQpq5oDLshrvcIDW5J9ySaWig0P5jO7PWhOw/7Tvz+KsJVdR7MrvadA1T95kWXalV8NJpdXts4prX+PFqsXXnaUemUk0AQTOuR1TqXBdr4Ppyddn/smXFz1FxxiTv4uC5HW5CPcNNc/bBWveq57PasnzABS4csGqVLFEJnldCczRXcjNnuzX5anRT9c4d4NBsQqbJYk4Sfkcpv+1m3NeX87kN1fU2K8qTqfMd/8dZk9uGR+t2Meps5fw7e9/8rfuDiaVfkmRSsX8iHD26bR8HBUBc8+GJ2PIUyn/sa8nfMyELhM859uq05F49K9q49zz51Osk2/nnp+VB6U3l2zF1OXtau2Sq1jEJUni/B7n+WV6WG8AhyvAqi5vYd9+jobkGBO0OoRYbSKHCr0VYbIObGLe6oY7mLdltA5XLk9D/QKABM2L5LoZzo8I57DWe8O+LO05eiV6I6TDXTXfzZIEpa7l45KjSh30Pa50NUUHYcWrYKshwreellXZ58YuaB6u++5V0rfdD0CSwwG2ChJcf3aVy6IUqY4GoNQaODDugE65sR/WKuLL2Fzlk11iVZaclJjtisVLbrrP6jRTNyIdDq4tqt8yNEB3+5XozNE4zMr3V4FazXvRypJ8x4iO9EuJDjinu06Y4nnt9CmJzMfnwi93w7KX/Tu4hHShK++nRpOISe8VUkkxQ4nvcapflwldJ1AT7gwB5SoVsSjvd+EWxb3jQH79A66W7c7jzq82cNrsv0n/azd7c8t5U/sqnVU5lEsSJ3f2Zj3Id819h05Lnut1YlRn7jruLk+bWbHRaKuU9M0uMSNHLkOSHBQZPgDg38Jf/dok22XO1MYz49QqfzeU7Aerr1jN0yMUN6LdOg1Os2KhVdVhLFX7PLTYRFrLNk27Eauh8lndeWSX5/XeDv+wJXd3La3bHw5X4IxKpaujpaAlkKXqFqX/TVjBI2eM89sX7irgYJUk+PJyyN+jRNr+dJt/5zXvUCP1tKxK0rG12tBSbMjdwG/7f6PCamFNidfF4pKSUjDG8FqRmdEVlXyeqTyMROiUh5UyR/3EnEHdPEU+wvXKEnof9X6mfriKc15f7uNT2Hix2sEpsfTgEe4srL9Y/fKae/hi0i8M19/n2VfheuDrGV9zztAuMQlMjbsUAJ1cCm+eqFhTS11CbW0VlxeXWC1wWXM1+g6YtGrK992C+egFdAsbzvOX+YvV2ojRK5ZVhyTRX7O92vH6LnkfzPc+iOaVWRmn/o/eqsMAfB7pv1r2a3gYD3fsxkUdUpAliTM6n0GcMY6U8BReG/caoLhSbKvYwckv/skrfyhV1Bbt2Os5R6qjTEl3aVrmd+5ecgIvXfEX8fG9CYROraNrrLIaVKBWY3f5/tYlYNzZAACsTvHQ3JZpN2I1VD6rf2/d7Le9r/LPFh0/1Dhc/m3qACJJ0PJYtTHV9vVJrh68Ea53WVbdfo2r3oS8HZ7jT2tOZru6hjrvaa68yeUBLHQ7f4d1HwKQVZbD9Qse8DtssVX3datG3i7Y8GXg8pcCD1f9chV3/3033+/wfuecVl7BmEozTP+L/lYzb2fn0t+qLJnGGJUglNIAiegddn+rU6zDQa/YXs0y744RyoOtA4kOUh5bMkuwuzIUqJoQYEVUxwYkkVIwaNX0Sorg9UtPrnasd3KPWvtq4hRhZUWCnC2Q+Z/3YFngIDa3ZdVgSsOkV+M0d8RWNBKVJCHp6unzC2jVWiJcBoI+Wn+xasLMW8/dydL/ttZ5Hp0aLlAtpbt0hEc0n/KR9iXPsSPRXaq1/0HrFXz3jLjH8/qkDid5XheQy6GCSl5drBhylu9a6jnmRKLc6kCl9S/7G2Oq+/7RKVrx061QqSgprZ9Y1aq853U4xENzW6bBFawE/vTrkcUWbxAu8RVbQjeZEOB02UR8fYMEoaNnjxP4devSOttFeNwAXF/3a99nj1bDaoOBJIeDr5IO8HV5b5bn5TMvMoJM62U8df3NsHsRdBsH6SMUcZu9BVxpkXDY4POLldeVRdyZe4TNpb/7jZtVVkLnmADVhdzIMrxzihL0pdbCgAsb/Dc4Fiis9FoP/963yfP6pZw80EcqZUbt/qI0LiwRyqE0gKXb6vAGtryTege64nwGDT+7GWYOOtdqTLFahduS6s0G0IQcQyP/D8qyoVfNy+c1EWXU8uCIx3h27ROefQlRabX2GZiaADshT6NGBqSsjTW2PajR8Gh8LGUuq21ceDK6GoKC6kuMLpJScx59tdu5VP0Xv2rHU2J28KR2LhdZl7L023UwdHnAvhmHijBoVUQdXcHLOsV39JBTyRBiA34JD+NvlTfDwKCEQdgcNrYVKEHE3SO7khzmjcDXqrVcmTqWeZlLiNUc5S3tK/ziGAWcTWHRanB5lJSpIONwNvYq6/eJ6rpX5qL0EahlJQ1VXoUS2FuXG4BG4z2v3VmPB2VBq6XdWFZDhaNkD2pZZoBFCUpQOxpXwaOtYnZbVoVYbRVcP/xKv+1r+18fsF2Ey7JaKXltUTOSEnkuPpY7kpSblhx2iElJvXgzJpofwvYqAmjkdIjvAX1cQmbjV96TZvusMix+gswC/1UHgMMlOdX2+ZG92ZudYPsvtbc9htmQ5XU3+qdQSSemcerRAoS5LOJVlj0jTUq6JUsAsWrzSV/WdcipDL/o3marSKdyBbr8HB6GTlIEtewSsFJTbklaA0x4Brqe0qjul/e7iGe7eh+OosJqLxF+XPJQdCoDB7RaJe3Vz3fycWQEEzqmclij9gZZ2a08mBDHv0avW0V8WJSfMK/6p9bVY1UhJVzJ07pLp+UF7Xusu8RJvFHFRWrlYfUU9aaA/f47WMik9BVc8d5qNKWHPfvTVMpKyaIwEw8nxJHnSmH2ycRPmHfWPL4+92vWXbWOO4bdwSunVk9Dl5CkZDco0EhMVK/ldd0b2CqKiWaVp02lSubqucpKgN7p5PmcPMaWV3CtqXud71eSJCJdCVMLrS6f1TrcRtypqwCsDiFW2zJCrDYFWeYJXWf+OHSEkTbli8hxDGV021d0kO16xRdLoxJuAK0BSZL4dOKnXD/welZevpI7h98esF2UUUlLJEtO3AvAR7TVHzhyDYqYcIbtZuQzi7h/vst6NPAS5ffG/3nLtR5e59e3m91r/Quzu4J36hKrm7/1vi4SWQRq4sPNn1bbZ3dF8eNOiH/qI8rvE24BIDpc2V8RwBxl9RGrWk3zfpZ9rbiRplLA17LarEPXiT6xv+d1lCG61rZR+igu6KlE8N+cnEixSsWsuBgytRqejotVrLwAZdns8/lsaWSZ2LDa/YH19fCAGd5hNADPxMdyY1ICuTnrSYx9ie9cGUAcsoQ9wNL38l2KVbKg3MqCrYXVjv/Q4wTP6wRjAr1jvH6kerWe6wZeR9eo6v68nV37/jYZuDEpge/Cw/jfJ1eyIsz73itVEuE9XgQgxunk7EHX8nq5RPjJd9f9hgGDKzivwq5Yfet0A/ARq476uCAJWi3tRqyGJMBKkmDSmyRc/zdl4Yp/l/fWH5gjRZW8tWQPB/LbvgX2wT+9T9fCstp6GJI4hNuH3V6tprgvkT7+cZYACkFChUnjjQZ3WuPJs+3iy3V72ZNbxr6YE0Gth9JMeG2wYkX6xXvDkYGjGkWsPpeTR2/XzSWztA6xuv1n7+vcHcJvNQAZ2Vv4r2BxzQ2ilUAUTr4LZqyF058CIDZSsRRWqCTkKlWsfC2rmnosyTYFi9Nb3rdrtOt7w30JNsVnNQgYwr3W1Prkjb12wDTP6//03vbbdToodlkty7Kx+3zGDE6ZGJP/33hAB/+UTRH1uOy7RXutkStMRqYf+JZDkbk8mqC42aglmQpr9aCiErNXtBkl/5ysu7RaVpQovqYTu07k+0nf16uELsDJHU8mSmOiRK1mhcnIowlxzNbvr7H9+WWViiX87l0QXbvLhRu9q6CEoUKZY51uAD5i1SYsq22adiNWQxVgBUDKII9YK7PYeOBb/+WXUrONWz5fT5f7F3DSSz/x8qqPmTn/b8osbTs6Mbvcm5+zuW9wguAS5iNWK1QSl6b6L3n+PPkn/rxkMdMHTgdArc8hrOubGDt9wGmzl3Daa6vB4RIdRQdh6w+evkWSlrlRERzRalDJMv2sVhJdATwf7nqSp1Y+FXhSFQWQt9O7bSmGDV8oKbUEHr7ZHNgP0UO3scpvSYKEXkrVJiAu2i1WVVRU+EfM2+3eG7m+mT/LZh/LqlGtrMy4NUddy7rNzQkpJzA0cSiX9r60Xu1TwlMYGK0EF/2t8ZZnzdeoofiQslGahd3nfellmWijYr1eeMfJzLl0CGN6KVbvx0qsRDicvBA1pM6xO4R38Ns+RHUxVllZPZiusMJHrGKhUKXCDnwUFcHFHbx+qCaNiUhd4DKmgdCr9ZzZzd/P2aqq/v+MdTh4IyuHW5yuh+kGmNOjXZWz3BXG6gqoU6m1ntRtWwo2c/735/PXwep5YAWtn3YjVkONWlLEqiw5+WLNQUrMNuwOJ28t2cPAx3/n1z1LCOv6CuE9n8KQ8h2HNA9wx+vn8My8t0I888ZjtRd5Xgs3gLaFWqUGl5Viu07HVr23Is+XZ39Jp8hOhGnDuHnIzSSaEj3HNKb9oKrEWdWiUbAHUIIzTu7Qk5djlawENxYV081m5+qSUkwua97XO79mc26AQMR9rsCw+F7gTg7+/U3wv6lNfbvtir1F+6vti3A4eTZXWd6le+AUSIlRXjG1astG8su8Fk5ffz6ttpnFarJ3qd0gVSDhxCNXQ+wHoFVr+WTiJzx8/MP17jM8RakGtzLc31rtKHSJ1bIs7D5vyyDLRLssq32SI5k0tIPHf/WiK39lefepDDk7vc5xU8Nrzm19b0IcO3RazJXenKsF5VbeX6asjGixI+HEoi3m1E4duCMpga8jwnH4/P0r7PXP1+rmBJdrQiDeOZrDbQVF/H7oiJKxYtAlDT6/BuV7KtedVSFAJS0/JJVH5Dyy9iH2Fu/ltr9uq7WLoHUixGqQcIu1Q0YLkjaPzYeLefG3HbywcBu6mGWYOn2EypDtWe6qVDtZnXCETWW15LFs5egdmZ7XWmFZbXOoUZb3bkr2itHlly2nf7xXTGhUGl4d9yq3D/P6vqo0ZVSrtm1VbmzPRnYFrbLkH+lwcH1RCXQcySCLlXcz8zzNl+zdQTWO/Kv87nqK//L/oVWQmdGId9g+KS3bWW3fioOHObfMJS4iA4sYg9aA1vVve/bfRzjzvTc8x+w+bgDqZv4sV+q8y8rnFb7DO9pXPNuqNnhLmjboUtSSjiMG/5WyokzX/6nwAE5fEegMIyashof72G6oxtzjfVirhWh9dI3Hfg0P45akBHIKvbmQ3/r2dyJ+uxP94X/4Sz+TFfHPEdnVjF2S+NtkJEvj78qVYEyocw5VGZFcsxte/+P+j+kOI/orv4U7NsNpjzb4/Jl6pQrfUpOyMqSqq9StpELt+i6R61MWV9BqaXvfDK0Ud+qmEo2D8B6z+O9QIUsOLSG896Pokxd42p1dVk5Pq/fGUK5qu1U1LJLXMiPcANoe3aP9I3A19mSi9NVvkgPiB3D9wOvROJVoclPX1wjr+Sz7fANxSpQHl2/ivNfzpxPmor3qWzhHqUrTRx2Bw6wsMy7J9PFNdWNzLVkaY2H4taAxQmI/ZV/VJOvHMFpbvt92b4vVu8g8ueaHX0mS6I0iQHLCCjHHzvUcszvctdZlJFVDs5U2DLNPWVSLJHGG+l+Mru+SJqWuChExhhgu6nVBtf3OnV+A3Yp9l3/6tnwpnEhD01ei6vpbZWk03P7ZKk9J28l7HuZSzRK+1D1NRymP1LIt9PVxR3X71T41+ilO73w6/zfo/xo8p9rcBkxj7oe7d0KP0+rto1qVLIe/z3u2uo5YCZVaiJx2gvg/BglNlVrUqw7upMC5EEmlLFOcXVbOD4czee6UF3mr62WcXaYEWDnb8NNepY/QFpbVtsf/DbnKb1st6WtoqRBnVJb2JZUdlaaMX8N97nSlmX616GVZTbcOI5Ubk6tsp95ZSaSsWG13lvyLU65inXWJmB+yj3JeloryO3fD6U8qxw77+KJby2HxU/DXs1B4bJU3BlA5le+OmwuLuLiklNdyfIozDL6s1r639KkuqgBsDsUqqIZmX4q/ZegtnteHNRpsgMoVmCqFOMCqsUwbMBV1FX/bLLUGfnuAooIqlnDJhkEbnAeC2oooaGQZnWTj63WKO0JPDlVrU1mW7betklSc2+1cXh77csAH1/rw3MnPBdyvDUL5Xklt9tvO19Txd5Sq2+pVTnW9K3wJWg9t85shAKEqt+qmqs/myiMZmLRK6p1H8/J53qyn272HkQZeRNL4JxjZT4mcdrQ9QwKglPOz+lw9jjZoETnWmdBlAs8c7/WN00i1p9PpFBPrt+2XRaBgP/k+Sc6Nkk9bd1YCaznndFLElIyTXS4/VyoK4IMz4D8lHdPD1r/Z5/yGB/76HCJdQSRlPhaV9Z/Aslnw9wvw3qlK/2MIh6QIu15WG4/mF5Jqr//qzOj+FzDIbKm23+EKsFK3wE28X1w/+moVIfR4QhxXpSbh9lmVQhxg1Vg6hHdgcPwgv31HNWpY+z75an9BJUl21AECjxqDryvAE8c/5nfMLknosPL233twOGWK1bFUpbJwn992pC5S8WdvAud0O8fzWiepuTa8F4+knNakc7p5YMSjqGiAYUSSKFf5yxynykGptTQo8xG0HO1GrIY0GwCgUfl/gPSm3RRpFGvFWEMHmPIjaL0R2G5LZFvNyppT7r8UaW8/l9IxRb+ELp7Xklz70uSgKjfjIzqfaOTig3435RfHPOs95rGoyNx0fC8wK+4Hn/73t7J7zXtwaDUAP4V5rbVZFYcg3OVPW1mgVMgC2LvEe+6KPNhxbBUPsLu+NfS+wjIsAS78oO7OESnMzvH6DltdQted1qelPsXbbN5sBFv1evbrlGunLboBuIkLS/TbdvuAVhWrBFGQp0V4l9Mn96pe7e01w+tkFVdw6TsrOWTzLtEvMhn5x2Cgsopo9q1KFQyGJA3jzgvnc8kZc4Jyvkv7T2T91Wsa1CfNptyHryguJcKVd3Zfocgw0tYQCiNIVM0zqonciCxBuNNJ/MWfQnxP/+MaZcnVIbXN5YjtuYf9tu1t820c83SJ9gbjWJ3V09z4Mn3QdK7ocwWdTYpo3WTwF697YgcAYHR2YVyX470HNEbcN+g4nZ1hiUMBWLRPEajYlGXtLTotDybGe7qVWksU/1V3la18V9Umt5W1o2sVJeOLut9oO8ImBRCr9+yGgRfV3VmSCPPJsVpqUZZVHa5SlHXlrWwuKl3L2W3VsgoQo4/x285yLVHnV7HsSSVjgjbmU6OfonNkZ5488cmAQj9FOsok1QrWHShksyacB+Pj+DoinDuTErghJZHyKm4EwRKrX5z9BSd1OIlHjm94EFVdNNTy+1JOAW9k5XB/QSGJLneXnflHgj4vQfMixGqQkPG3SjnVSsBCZ5sdKbZ6tQ+tq2ZxWw2v2lOQ6bdtdbZVG/GxjcbnIcsu1b40FqYN44FRD9A7ajgAlTIQ530I+y9eSVtjVEf7d1SpvNZVaznn91HalaIk9i63W/i/5AQeSIj361Ziy1f6dj5R2fG/qWC3gMU1z77nKr+Lji2/Vbtrybw+JTkDoffxVS6qUKLF3UUBWuqGMDrVP8WR26VEUrXdW9LpXU73297oSgc3JzYaAEdFZyqPXI6ufHzQxuwa1ZWfJ//MZFclrarYJIl7tF+hwsnHySX8FBHGU/Fed4Aj0f6ZIxqSV7U2BsQP4K3xb9ElqktQzlcVVQOu/f4OmTGVZiQgwaHccfflH669k6DV0Xa/GVoZshw4KjHNqQVN9cAVncZVnrWNGhJyyvzL9I1O6RiimQiaytgkJd/hGWnn1au9Qa1cz3bZCiOuU3Z2PYX9KNd0hDameicfsdrPtcogaYqxOZzMz9vESqORfTr/B75KZxELdi/iswGTFAtr7nbFXcAtVuOVqnGUZh1Tla6srtUYOcr1oHDCLbW0ro5uwrNoXH+vkvIiAOwui1NL3RBeOOUFXhn7CmkOxUrmTh5fW8BQa+f4lOP57rzvmD1mNgAbDHr2aDXkuNwBHOYO2EsGo1c333s8OeUE9D4uaRZJIlUqIJ5isvXVi9D8QZnf9uCEwc02t2CiacjH3ccS6y5OklW4N8gzEjQ3okZmkKiswbKYXNXK5MLts9pWLatYFYtMst3Oizl59B3d8Jx8gtbBaxMeZnfh5XSN7lKv9katr1idDhHJ0P00Cr99HIBoffVADl+xGhen+LpKKjsllWacZYUQoKKjVcrm/uUzQZKJS+zNxAMrFetqhctfOq6H8ttpU4KswuLq+Y7bNjbXA275sFshpQt0HN6wE+jCMMgyZZLE1M++5swBp3JqiisbQAtp/ih9FOM7j+dNSY3vt2BbdgMA6BHTgx4xPThuyyD+zdvI5A7eQgzWAsWarNM0n1h94/S3sTgsjP9qHCX2cixhCVCcxRmdZP5yqihRBb5PndPtHMK14TVaaFsbWhmsdTdT8HkAcltWC8uqZ0YINVa7k1m/78CgVXPHaT1RVfEn3lm4k4eXP8wVfa9gUo9JzTKHEa/MxqAJ47MrL6Oza0WgtdB2H2NbGR0SAtdPDtcnBtyv07Vty6rKUgRAF5uNoRarUhte0CaRJImesT38XAJqI9x17VrsVmSVGvpPBkMkpTbF2h5vjK/eySNWy4g2hnt255bmo6n0v3HcFKtkDJDUZnBZEb92um5NFT6BfaY45QegLEv57XTA1h+hzCedUxApMBfw/qb3KbYU1924mbC6vjMMpjjofAKoG5izU6WmzLXc7kz7hp9zH8HubNkAKzfaKiPKbVysuhnXSVnql33TuTmUe4S2GS2rKkmFUWNEr1XGsoYpn8WnT4sntgZfz76xfXnu5Od46PiH0LaRSoTahqykWL3WY7dlNc/aunxWC8utTEz/gXmZ03l3+6OsO5CP1e7g5p9f5pR55/LsP6/z7MqX2VawjUdWPMKeoj2NHmtrzhHGzr2RJxZ/6be/1GzFGvMRRZHpyBX7m/iOgo8Qq0FCraq+xAKAoQax6lpKdbTRL2eVVblZeywxhuiQzUXQsnSLiwbA6rSwK8d7I6h0FAGQFBZArGpdYrVwH1pzKbjcZlR/3EVpFQvClH4nVeu+F3O1fejCIdwVEDL3bMjdASteha+vhi9qru1ebivn8X8eZ+G+hTW2qYkLf7yQV9e/ylsbQlMmWUkZp/y9wozB8S9U6fK9AVZBOWP90VSp7m7Qto/Fvr4+VeA8OJXv/Oa0rLpxW6gv1peQpVZD0SHi5cDjXtH3imafT7Bp1FUSkUJ0zHEA7FcdYeWhTUGdU1N49o+/yIl+FJW2GG3kFm74+jvu++19luV/RKF9P1/sepetWd4MBodKG28Zvv23p8iXVvDN4We49pu3cbhqZ+/POeiptJYa0/rc+tqNWA11ntWYGp5INZrAFletj2W1LSYolh2KeFABjLkP0kaGdD6CliNMr1y7kuRgw6Eiz36rrDzApIYHcAlxW1YX3AWv9EflVNxgpEN/Uuqy8mlkmY+OZhNmCEd2+Od8rcTnYTBtFAy+HDQ673krC2HpLFjlEpHu0q3AWxlvcfnPl5NZlunZnr9rPvcsvYcbF91IidVbkrI2KmwV5FUqaZ+25G2pV59gsyF7M6AEmEQYA7hb1JNZ2f6WZ4snz2rj59YYqorV5EhjDS3bFoET6ivXeUuI1ZxKb17iG5MTcGb+R3hlkV+bWae8yEOjHuLsrmc3+3yCTbwlgF98TSS7spZc9CHD4vt4ds/fsizIs2o8K/K+89u2J7/G8swP/fapZa81eE9uHo1hd+FuspwrPNtry9N5fsmPABzKUkpgRzmc6Eytz6WqfTzGouRZnTFjBiUlJURFNa7yRlMYberE9KJi+lqsFKlVPBmv/LPdUf9VMWiVm7FdkrDb7Wi1bWP5xY1TVpZTKlSRMO7BEM9G0JLoXasCSHbu+WYjhwoqmHlGbxwqJfCpc3SA1QTf6jW2CvSoqQTKVRLFLrE6XhrK8MGpkDoMpz0CtdqM0xaFSltMpcquWO87joCrvvGey+hz09r0dbVh/z70N29ueBOAV/59hZuG3MS87fM8x1ccWcHZ357N3DPnVis/6+Zo2VEKzAWe8wB0iuxUy18o+Hy26UeWHFxOeZkSxTyuopKE2ManGRpepTBApc1V7rTxU2wUETodvs8hbTnAypeqYtVp9T5YOJ0t+0SwR6cjb/NX2OL9H256xPSq8Zpv7RwpuoMulk/ILBpXd+Or5oO5GOJ7kpy1iZ7ZVnbpdBSaW09hAK2ulKrFLM26CgD0TicWlYoyjfczW5q5GYbVI1VdFe7984Vq+9bmrADOJ7tACTqLdtDsVewaQ/v4ZmgFSJGp3FZYzOkVlST5VJTRqgKLVa3Oa0Gw2AIscbZynC4fVVUbdWMQNB6D2mVZVStfpq/9uVvJDqFSruOecR2qd0ob5bcZ51TalqhULDe5PgsJZ8DEF0CSSDIqKXVOST5fOaayYbltI1xRRZCecjfEdKk+nsbAzsKd3PKnN1J+4f6FzFo7C7vT32WnyFLEvG3zqp4BUKypF/10EZctuIylh5d6+7Tgje7nXYt4Yf1DrM77lc1mZemyu9QZnanxbgBx4x7ml0Pe9HOVNiXHbktbVvVV/G3bclEAX6qK1au7P+B5XWKuwWUsiJzX/Tw/H/RctQpblb+tQVN7xbpWjRTDpuwbyLf0qrtteKI3z3lkB06sVL57ilxxF62BFKvii396eYXffqPTycmV1fWBtZ6rQVU5WqLEFVxSUsrJrrTapa5z5ZcqD8IRNWQ2CjVCrAaLLifBhOfg6u/90mpoA6StAtBrvV8UFmvtydhbI27LaluP3hU0nN6xvVFJalS6QiSN8uW3/MBW5aA9ig5RAZanT7wFblzu2ezvulG8Ex1FgVqNRobTOnmtJK9PeIzr+97NS6ffiiwr11iWtVLJu+pL2ki4fQNMfNF/f0IfPtzsv4wGsOyIsvRXmXkRUaXTeOyEJwDYXrCdh5Y/xCv/voJT9kZM7y/ZH9BNYGdu8wRwVWXN4a088M+dnm2908nkknJOPuWlpp245wRS7F7RVGlXvoNa+tOsU1d5mG8nllWD2oBGUm76Ro2R204+xXOspNLW7OM/eeKTLL54Mf3i+gGQp1ZjrSJWjZq263KhVjfySo3qQJSrilWFJb+Oxi1HlD0bgBGVZuZlZvHt4aPMys7lPSmVrurqqzhmW0W1ffVBkpUCLCdVmjm9THn/lfYiAIorlTlEEFizhJr28c3QGpAkOOFm6D6OwqG3eXZX+zJ2YfAVq7bqtbpbO+4burCsHntE6CLoG9sXALVRcfRfl6kk+DdKtSxNJw+ECUoZ1hNc1oKNBuWL0WpJZlRXr/vAgMSe3D5yCuF6AzgiAPjn8H81n3vEdDj1Yeh+qrLttLNg7wJlvtoYknT9/Jo7LUkcPtyb534oAGBT3iZ+3PMjH27+kFnrZrE+ez37ivdx6c+BA7UKzY2zbDSU1Yd3AiDJMn8dOMzqA4d5sPe1DOl/XNNOnDwA9diHPJsWl2W1pT/P7kBTN+3l4VeSJG4fdjvndz+fnyf/jEnrjV3oENP8IlGtUhNriCXJlARAhkFPhsH/b+1eIWmLTB6irN70TWng6kJ0Z6JcaSbz7OvJLCkI9tQaRYVK+d+YjR0YZJPpabMxIboPgy/4mAuTh1Zrb3U0bjXW4FSMC3qnTLTr72BwKL6wZVblbxGpCg/cOcQIsdoMaHz883TawE8pOr33C8tqa7uWVSFWj00GuKKdDalfoQnfwqajyhJSwLRVvoy8ASQ1ne3+S6EmOhMTFvjBLg7FheCV/571s3r6oVLBKffAyXcDYHf4+HfZCtm94RpGh9/n2SfblZtcQVH1L+ZPt37KlIVTOO/7moskuF0gmpu8AiVFzSmVZuKdTtTDr8Uw7u6gnFs68RZPCiCL3S1WW5aqK0/txWcVYOqAqTx90tMkmpSHsBcvHMSwTtG8eeWwFpvDSR2UzBrvR1eP42jLbgC3nNqTt686ji+mj6q7sS+mWE6y2IlwODGrLZz7v2ux2UOfdtGJ8r1WHDMY7toBDx6F/1sCkal06H0am/Yd5K0sb9CcpZFiFVdWlZKuk4hx5Zy1Scp3WZlDeQCP1LV8zE99aD/fDK0Inc8XsK4GNwCtj0Vh1e6jAdvUhLxjIXlvHAcHV8PSl0JSG90tGtqLJUTQMPrHKWJVUjkwpn3Kjlwlz2m32KTaO6o1MOxqulr9l0IHRp1WY5drBijWzUpnEU8te7PGdsr5FcFb7iNWzdkTAVi4NhKDozv2ii70ild8YmVHOLKz5uDGcG0E0eZJlO25i4pD12DOUnxozXIeVke905I3GnOR4l4RJuvh9o1wziugDZLIUGs9law8YlVu2c+zvkq2lPbisxqIS0ak8e3No+kYEzhDTHNwSsdTajzWlh8MdBoVZw5IJtoU+AG3NlJHzeDDLGXJ26rZw8R5M+vulLcL/vuMTQfzmTV/KbmFwV1Zcbo+h2qVRiluovO5RnqfBWe+wEmVZq4tUjKuWJ2NW421S64V0ehuWAZOd+1TDAcVsiJaYwx1GBxCRNu9Wlsxep8nVn0NT6+SJKF2XaDzfl9B+l+7633+D3+5nnERVuZ/PZnyv56h/IeboCyn7o5BxOuzKi6hYxG3L5wbtVFZShqQnBqouT8pg4lzOvkkM4tnjmgo3fkI5/ep+aZ69XHedHTrjtTxOXFl3yio8KZ2sRWc7J4luTunYz7wf7xyXBF7HxzOk+cPAMlrWSnd/jQTo1/BqDESZ4jjio4vc2jf8cjWBBxl/bAVeufyRsYbdb/XJlJhVh5kVZpkiOkc3JOrNB7LakGF4sumaWGtaKry/SjpI1p2Au2c5LBkOkVU93m8su+VIZhNKyE8mT4+D8vZ/Fl3+sgfZsAPM9B/MpHbN05i49tT4egG2PxtUKbkdInIgIVZJAmOvxEeyqJclwaA1dm4B2Wbq8iKURuG0aRkUrG79pWpFAEcH54SuHOIEUqjGfBd+q/JDQC8kbf3RbzKu7/9y97cshrbujHnbGVOrHKRPZ4Qx/Fd0jitUwcO7fm9aZNuIMIN4NimasobTZgiIpNMgYtg+JEyBIChFisTO/Xmw6vHcO6gmkWuTqNiiGkqAKXWOqLwXZbVWdE+qbJ8vuaMmPlM+xz9Fk9BNfdMukZrcVQqN3NZVoGs4euVFtSHHiGp+Alm/1I1CMN7M/lo80eAkmVgyq9TOFhysPa5NQKrQ/lO0GkbkFeyvkiS593kViiWIk0LW9tMVS25sT1adPxjgXO6neO3veqKVdw/8v4QzaYVEFc9XdeuggMAlGxbxDcvTOazd19Cfu80pdBI3m44tBqAXvYdaCUHp1kWwzunwDfTYO/fTZ6S05W3Sl1bNTqtEb1G+R44QGGj8rO7q9+F6cMxucpT21QyTqdMmUq5p6fEtGxavvoixGozoPO1rGprdqZ3f0/flJzIzOinyS6q/UZcaatgxK/VAz7KVSp2FO9r3GQbiQiwOrapqTTr6Z1Pr7tzotcqq9UbObVPUrU62FXpnaCI4EpH/cTqUpP3c3fdSV1Zes84njy/P1eo/2S02pXQv2AvvcwZmI9chr20L5UHr/P0OVqkYvXewA+PtiIl4KFnTE+yyrO45+97WJ+znpf/fbn2uTUCi6xYUIya5gl6sLiW3Q+7LOOdqiTpb25MEf5WnPbsBhAq/m/Q/7HookVsvGYjG67ZQJg2rO5O7ZmOI6DK99eve5Zhd9p564/reCJ5N19LH8CRdVT+8jC8UXsw4/71fzR5Sm6x6s4gURMjSEXvdHJIVcpr/73Gnwf/rPcYSvU75bVJH05YmFK8xSFBdlkFxa6Pflpi63xgbDdiNdQVrHzxFatadc2WVd+8d+lJTpxH/6n1vAtXv+p5bbJEoHF4byyZRYWNmWqj8bgBtGG/J0FwObfbuYTr6iGqtAZwfy56T6zXuZPDowGwOOtYfVDr8A2XGGY2c88ZvegUo+fKUZ05J2qvX/PEFU9gchioPDwFR0V35lw6hJvHei0vd53ei4+vHcl/j5zOe9cM55Fz+mHJVzIOHCw5yGvrX/O0zcjJCHo1OgvKcqVR1zzL4+WuVGDlrgTkfaSWLU5iShzgty3EavBRq9QkhSUhSVKb9lMNGoZIOPlu5vhUcXt/24tc9uO1fBalBF7u02m5PDWJ8oOLPG0Oy4ovZ9VPeOWhDU2eUr0sq0CHiDjOL1Ncdt7f9D63/3U7Owt3Umiu+/5v8fHjDzNEEh7hXQXbdmQ/FpfBICWxHrlrQ0C7uXJnzJjB1q1bWbt2bainQpjR++QaZqjZmf7u4d6o3kqVioPm2uv97tjyPQBJFg1lhx/h1PCPGVihWJAKK1smlY4bGWFZFfhzUa8GVFS5cTlMfgcGXlyv5j1cllWbXEFxhY1FW7P5ZdNRtmZWue7VOg5rvNaJD47mYPh2CrzYDXXpEYYmuh7wxj8B4UlIeTu4Qr3Y035C/2TuPqM3d5/Riwcm9mHGuB6M6ZVATJiO0/slce3oLsjWeJz2cCwOCz/t/cnTN9+cT745uLkbzS7pHRawfGfwGSK3bI5FbZXUfjVZ7AWCoDLmXk6rqORNnwj7HYX+JZS36PV8E6E8fF9qeYR9zmTmRYYzvHMa70RH8o/BwAdREfQpXgoNyOhT9MdLlMw5Aft+r3HKY1mtoWy7my7JcYyp8B/rwh8v5JSvTqlTsJZYvP0iTJGE+YjVPUeVYiMGpxOT8Fk9djD5pKUKM9TsBnBNv2tYcskSOrl8pZ2OGtLyAGVHtrHfqdwIk0o6seL+05h9yWD0rou7sUmCq3H4XyiqXTQDyB43AHEJCRSGJlbPB1gjCb1g8GX1LuvXPUbxaZU0xQx+ciHXf7KOm+et5+zXl/HrJp9sGhodO3XKZ6K/xaL4ZG7/GcxF8M8bYHGJ28S+Sl5W4P/UC9DjWm7XqVGpJG45tSc3jOlezT1BqijgUvUSpPLAwU5l1rr9zuvi+yWref75RzmUV4LZ5UcWZmgGn9UqGJ1O+kstK1bdxQgArup7FcOThrfo+IJjFJXy0HpypZkVB1z3O1X1oKWjGg1/OI5jNT14Vz2K9OhorCqJN2KiuSElkTmxMfxhMiLPn177eDYz/PkM7PyNzetncYchjwW/POM57HQFOWnqsKxq04bRzRa4qMSqzH9r7VtqdlepkzEZwlGZYtG5Sv8eyd0GQEwrLbUKQqw2C5KvdaAWS4EkScQZ4zy2SSeB871Z7TYe+uk6VpiMSLLMnRc/6EnZoUW5uC2ORuZq/ecN+ONRRaDm7oD3T4U5A+rs5kldJZaVBMCkHpOadQk3NTwVFVoklR1t9BrCejyLNno1sgwv/raDp3/eSmZRpVJmVad8NnpVSY9FWTZYXGJSFw6DLoPIDiRIRZyp+ZdnxieCs46ci19dxQva9zjTHLiCVam1vKlvlQ4rLibK8BHLPn8Cs+smFmkKUBUsyAyxWNC2sD+jb7L8+0beJyyrghYnwil7MvMAJNjh+l53AbDWoOfnRD3RvV4io+sflKqr3+/WGwxI23+qtt+PzfNh6Yvw+SXclJzIWqOB3zjiOez+1tGq67j+u44lRhe48MreHCWHarGlmNsX3c9Puxf4HS+xKAYtvSyj1ZtAF4He9b43H1Isq1Fyy/qsNwShNJqBCIN3yS6iHgl23f8EZw03ymf+dzt/GkuRZJlpqZcxvLM3EbJOUm7Mvv4o9cZcAr8/BCteVQTqxq/r7uOeq3ADEPhw34j76m7UBNQqNSlGxZppSPkelbYEQ8p3RPS9n4Plm/koYwFP/ryBzYUHeTtG+cz1tlaxlGz51pviTR+hpLlyuSG8qnmdK5ePhydj4dBa+HkmFB+B7b8oKWrcHFSW7q6z7PA7tdOuLBceLi5q8nu9vkMkb8ZEs0LzJ0Wue0d8RPMvzQ0yW8EY3ezj+HJap9OY1GMST574ZIuOKxAQ3xtQSgw7fB60H+s7nT5Jit/6Ia2WPyMP41DV7GaXpVE+pPaK4uoH130I2xdAhbIqeljjFYPZGjPZecp+r89qHXljVSoYckXAQ3tLN2Fz2pib8Qt/HlnAgyvup7CyyHO81OUGYJBlJK0BVCp0Lo2+I0GpQJiobbkcwA1FiNVmQKsx8cfBI/xx8Ag6Xd3/fHdifaczsBvAltKVAFxkS+HOMx72H8tVps3SmCTBm/7nv71slve1vfY8bk7hBiDwoV6BVU1kaEqfgPtNXd7BlPYJGaXzeeov7zXdq8Llk+Wby9Od+krvmm/KoOon/GA8rPsAXukHX14O86r71ertEVQevsqzLdsUgVxYWUe2gnrgdN04l4bZsUsSKTYH/XudXEevxvFInlJicZjZzFUlpWBsfncDXzQqDU+NforJPSe36LgCAVd9o/iu3+JdPk+1ORhzwq30jkur92m26ZR7cP7hXf4HCvbBz3fCl1fgQObTyAgmpnXwHM7SqHh77usUVVhxurRybQHZbsJOvYcH8groYLPzw+FMz/7fMz/ng40fs6/Au+rzz6FNnqDPMteqkl6WPQGuxir378vjBtfzXbc8Qmk0Byo1yQ4HyQ5HrW4AbjxuALI94PFilbKcObxX9S90nau+s91ZCYufVH7qw65FsKCWyh2W2gO23AFWwg1A0FL4+sRe1fcqbhh0o9/xSuNf7CxQov0teafyY4cnYNSNcMdmJV2NL+7o+riedQ/sdh/wsVJY9HHYSwdQvvcOyvbc5Sks8PnO9xr+xuqgpyWC8LDmWZ6/pLSMTfsO8vHRHKVWuCG6WcYRCFod0Z3gpDsgvgf35xcQ5nQyvcwKkkRymHepXeuKC5GQGJU8iqn9p/qdJlOrZqtOS/HRPf7nt3gfXFcvf5YX4/wfBEvUapZGfc+pz7+Dwx1gpalHNg6NnitKy1h4OJNuNjs9LN7vho82fernN3//ypt5YPkDAJSZlXu63imDq7Lm2HDv919fi5WThlxf9/ghQiiNZqFhS+PupXR7gLrnFruDUtd/KS2mQ7Xj6JUPlcaWBctmKz/Fh+sedMcvnpeFcgCrmDnAkoYPHsuqEKvHLH1j+wIQoW2ZqkMX9LyA9894n3dPf5c7j7uT87uf53dc4+iIRVLKvjrNKcR27AMTX4DwBLh+EfR1tZfU3uXuSJ9iBP0n4/fZTfUJGCvNUkouuogyuFZDLMnI1gTUBmXcA2U7m5y+KtHufWjtZ7FwScIJTTpfg2hhNwCBoDVwpRTLygOHuaiDsoJh0BgwapTg6E8nfsoLJ7/ALxf8wvsT3uf2YbdjUPtXXrsjKYHy4iqZQGQHhSoVFZJErtq7/D/efj5fHv80ADk6B7bu72JWKfdTXT0sqwCyzwpId5s3o4HVpqXM5h/kuWDvAjbnbabSZVk1yLJntemGMY962k0J6wNpoU/9WRPCm7050PuIv3r4gLhvj3IAn9UjheWUq5UWHeKqV/nRRY+A3CXs1/o8kZVmQVTHWse0HV6PFvjOMZrZ9ktYrr/dv0FdYtVtWRXPO8csc8bN4e0Nb3N1v6tbZDyNSsOoFK+/tq/1A6DSZkalU8qsOi2JjO5Rpcb12S+DPhIGXQLuqFtfS6IpHs55BbI3/3979x3fVLn/AfxzkjTpXtBBS6GFsoWWForsVQRKQVQQpD+BKiJDZMj1qhcXekVUpqBcVETEAaKC48pUhlwou6zKElpGB6u7TTO+vz9Oc9K0pXSkzfq+Xy9eNCcn6fPpyUmePOcZwAOjgObdgQ+7ALcvAAdWmLTO+lK584NkQOmSiTcKbiDYvZIvltXkUlrZ/eJGJqI6PQn0bcDVhqrT0syYvRm7HsJfvwJR46VN3w//HtnqbHRo3AEdGneQtitkCqwatAo38m/AS+WF6bumI12hQE5+pslT5hTcRp/m4ufw4NK5UZvlNcJria/Bx02Jwb+/iG2lA6VzSyuz95sNwEBI+F4cDA1gWnYObsvlOOLiDK3sFs4UiIO9tAVhULiJiwVN3zUdY5skAgCUREDpQC7vwEh8qGqFg7eS8dATi2rwB2t4XNOoD04uwNM7xX/V6bNa2hBT2QCri7cySvcheFUy/1lrP3Hk/t9KJ0jtMXkZVf9CnQZCpjin3FLtIwhsZpwE+JZMhiJBAN2nsmpoPeKWVccV5B6E+T3no5WPZSo4TnIn9A/pL92Wu1yHIFcDJMfyUQMRE1ZuBL27HzByJdCir3Fb2dG3pAe6JALDFokVVQAILl295ujnwJZp0q7OeamY0/IG/DxU+DyxKwpTjZfPTt08dc8yH7lyB6v2XIJeb2x9zSnUYOORqyjWiOd/cenbcvaQ/4hlcfer1t+jzpy9gPDYhvldjFmTJhFA/1dMGnlCPEPQ0a9jpbtHB0RjeMvh6NO0Dzz04ntIRlG6yT6Hrp+Xft7mLl6q93HzhY+bWEH9V483KjyvUnGfAVYGTaOBV28Dro3RQqPFpxlZEPQyqTsSAPgrw6ErFusMd4rvSC2rynIXfvqN3oiXnj0Np0bWuXKVAdc06ktI12o3qUsDrMp1AyAi/JUpfjPy1ushd644s0BkUBiUegEaQUCaU+kH743jVf/CMz9CQSXIJhegw2/IafwmnnCJwwc0BP2bN8W4oACo86ueYNi4KAC/hJjlLOu/DLsf322ybUDTIYjvVPk8qFWqpBsOHl4h9m2rxPNNUnD4X7Ho38YfuqIWKLnzIADgH3v/Aa2+8v7nY79ZgaXJr+PnU2nStqU7UvDh9zswdb040ENTeqnFxcWz5hlqquyVn1aDxRkSGGPV5gfxHLqpuY03fjqDeZtPQZt2GK4F6RX2LXE2vpf4RDyBU0M34llv4yBPP88aDFSVK4CHxO4EcgAuGtPuWA/riuF0dYJ0O08tfqYrqVw3RblCbGCzclzTsAJSNwAyfiv69WQ6wl7+Lz7/8yAAoIlWC1QyB2K4nye8S8ST5YKhK8DpTff+Zbk3gB/ECYyTZCG4o7uAjMIbOB14Gp/4is9zUalEYW7VK/EY+6xa77xszP4Z5ioua27M9Jo9iWdpa0q7+Ir3yZ2Ahz8y3RZaOjI/z/hhtGh0BPRqYzed41kVvzASEVyCvoOTVzJ+v7pN2u58ZAVWeP4T0ZeW405eIUpKFyJwc2mAVaueOwy0Gw407wnEvl7/v48xO+MrFz+X/QoOotGh95B66Bco1sTC/eSqCvs6q8rNtuHfDnHB/aSbsvtNXVVep8elHztrjRXOKXdzMCf9K5xQTIO8tCX1tlqcJUBpo9U+2yy1nZFJLavG9vmXfjgJAFA6i4OlAnWCOMdaOS5KObxk4mXCcyolcmUCcPeKOEdkZe6mAgAKBQGL/Ew7iSu9j0g/F+Tdp7IK7gbArMeXQ79Ec8/meL/P+wjxrP60MwCAZ/cCiVuBlgMrvz+sN9CszCCnkBjx/zKV1ceim6Jf037S7VuFt7D18lZkF2dL2wZ9bWzlUJbpfeDS6A/8X1AgtH574bY4DOrSqas8XBugsurVFBizHkj87337uTPGKgptLLaWZinkuB2wF7Lma/BYUCAWlBv9DwDuyooNTs0DjQM53ZQ1HKwqkwPtHwYAzM42fkEenyOO/BcANNKJV3luqsX+/E5km5/ZVjnA6vLly3jqqaeQmZkJuVyOgwcPwq2epm6xBsZuAMaWVV+vDDiFrEChQlyVoksVyyB6O4cB+iv4xNsLmzzcse3qDbik/g/oVMm666V9UT9390e6W/Y9nzM7PwtVfeRL3QCsdGk25lgi/SPxyyO/1O7Bbo0At/uMuB80H/hskPhzSOkgr1zTy3ydmjTFvrMPwMnzNF7c9yIAoF/Tfvhw4Ie4W5SLTK3xw0Rbpnv6p97iuf2Ztxem302DpvSccnf1rl0exliD8Q+KBm4m4RPv+3+5DG9UcdyJ3CcU315PR45MjsY1rawCgEac7L9NiQZLMm/CQ6+HR5mGLx+dHlkK4I5GnFOZW1bNaOLEiZg/fz7Onj2LPXv2QKVq2PWqG5rhIOjKVFYVXh9BU1pRHVhQiASngHs+3s/XuDzqXbkcVxUKIHV/pfvqr/wJADhWZhL3Ua1HVdgvp+hOlWWWVtzgbgDMEYTEAKO/AEZ+DDSJFLcVZAE6Y9/UXq0ag7SmHza7r+1GkbYIv54/YLL9lzN/Y9jyfbh6Kw/NNcbn2O5m7EPqWo3BmYwxy2rp3bLa+45o26PiRtdG6FCiQY/i4totytF6iPRjbGERuhWrAZ8wYNYpIGqCOH8ygJzSGUycYJuf2VbXsnrmzBk4OTmhd2+xX5ivb/2viW1phpZVKjPAI0/Ih6Ea26OoGLLie1zWBxDu1wa4ZbydoZCj9aXfASLA0PJ56wKwaz5kKT8BADJVagAyrBiwAl0Cu2DTedN+robO2PdCpZVVgSurzFF0GCn+r9eJc7WSTqywls7V2rmZD5TwrvCwfdf2YXfaQZNtzgG/4kKWDnEfpMOjtfEcesnfON2WUsaDnRizdoOaD8Ky/ssw84+Zld7vSs0xruNg+Dh7IdwnrOIOggBMPyQuf+7uX/MCdHkKaDtMHCx57AvAt4V4GwAGzYfvGvGKU4EgzgZgq5XVGres7t27F8OHD0dQUBAEQcDmzZsr7LNy5UqEhobC2dkZ3bp1w6FDh6r9/BcuXIC7uzuGDx+OqKgovPPOOzUtos2pbLnVMI3xg8pdrwciE+75+MhA0292V1WuQHYqkHXWuHHPe0BpRVUtANeU4u9s49sGroqKLTgF2qqXjTTMsyqT2eYLn7Fak8kBj9I5XvNMuwJENTV2ntEViT9nq7NxLjsZAOCrM149UflvhdL3T+TJK74NR/lHQVGN1e8YY5YlCAIGNBuAVbGr0COoB15tFo9+zmXeB3RKzIyegfEdxt/7Sfza1H5CfkEQ34+cPYEeM4wVVQBQecJLZzpXldL62iirpcalLigoQEREBJ566ik8+uijFe7fsGED5syZg1WrVqFbt25YunQpBg8ejHPnzsHfX/zWEBkZCa224tQu27dvh1arxb59+3DixAn4+/tjyJAh6Nq1KwYNGlSLeLZBmg0AxsqqUq8ESmdOjRr0HtD+sXs+vn2A6eToW909kZB9R/wgDSidzDj9BABAAyC+aRB0ggB3J3cEuAZAEARMiZiCS9mXkJJ6EteQiUJtQZVlLiktqwu3/jBH5BEI5F4X+62Wmf/fy0UFFIo/60saQe5yFXklecjWXQYEYFBBITZ4GrsKlAT8UenTrxm8BgL3B2fMZvQM7omewT0BAI8D6PiFOEerWnGhikfVM5kM7mTaoKQUHKSyOnToUAwdOvSe9y9evBjPPPMMEhPF1RJWrVqFX3/9FWvWrMFLL4krsZw4ceKejw8ODkaXLl0QEiJ+M4mLi8OJEyfuWVlVq9VQq9XS7dzcqte0t0aySroBaCG2wDzu0hmBkVWvEOSmMj2MpxV6FAkCXArKjOjPE1fX+KLr+8i49aH4O/Ra6QNxeqQ43c/Yrx7DNW0miqmoyt9ZVFo+T4VzlfsxZpc8SgdKlGtZbeHZEn+UnnakE6eSWXpsqfSNdPrdHLQpKUHJkIV498h7lT51Iy0g5ysWjNm07o0fxoFbWxDtPdKi5XCD6ZgfJ6F6q2RZG7MOsCopKcHRo0cRG2tcBUUmkyE2NhYHDhyo4pFGXbt2RVZWFu7evQu9Xo+9e/eiXbt299x/wYIF8PLykv4ZKrm2pLJFAXSlLZfBqkaVPqa8krsPSj9rBSBZpQR+nAy8HQAc/wpQi52rPz5prMDOjKrYx8ZNKY5oLIK6wn1lFcrEyqqXDUwmzJjZ3aOyGuLWBkXXn0DB5ekI8a7Y395Hr8fovAKM843Af2L/gy6qwAr7OIFbVBmzdavi5mNJ34+wYug/LVoOd8F0JiWljCuruHXrFnQ6HQICTEeuBwQEICPjPkuAllIoFHjnnXfQp08fdOrUCa1atUJ8fCWTdZd6+eWXkZOTI/27evVqnTJYgqGyqiutrBIRNKWVVedqtlxOajsH+ef/BU1OJADgiHPp47TF0jKRBBny5OLAKWd9cyS0q9gP1sNZrByXQA1cO1rp77pbfBcZTmIXBW8escwckdRn1fR9rVNTL2hzI6AvDoFHJXMqGggFN9EjuAdC1IUV7nMqv8IMY8zmyAQZYkN7w11Zg1Wp6oG7wvT3K2W2ObuSVU5dNXToUJw6dQqnT5/G4sWLq9xXpVLB09PT5J+tMVyKN6xgNX7NIWgFsVO0s1P1Kqv/GNIOp14bBW+hLQDgfy7OKLcEMP6Su0MV8BsAoJG7rNI+cZ6lq/nkywVg00STqXm0ei12pe3CkO+NU2V4u3hXq3yM2ZXSGQDKt6y2CvDAt5MfxO65/eCsMG3B+CDzpvGGthgoKUTP7Jsoz7myZV8ZY6wWvJSm02F5yGyz655ZK6uNGzeGXC5HZmamyfbMzEwEBla83GVOK1euRPv27dG1ay1H1FmQ1GcVemh0euy7cAu60spqI/fqfytzVykQ7d8dRDKcclbhUo+p0n3nnJwwNcg4aXGu9lZlT4HGpavm3JIpgew04MJ26b45u+dg1h+zUKg1tgb5eNx7/lfG7JahZTW34vrfD7ZohNDGbmjm2kHapizyx4DCMv3AtcXAzzPx0N1MvJurweR2xnPVXV91FxzGGKuuIGfTz+hmilrM5WoFzFpZVSqViI6Oxq5du6Rter0eu3btQvfu91khpo6mT5+Os2fP4vDhw/X6e+qDUGa51bxisSXTUFl1rWGf0D4twqFXiy/O9PbDgD7/AAAs8fXGbYWxJXVet3mVPj7MR1y69ZJQ+oL+9gkU3fwLib9Nwh9XjSOX2xTrMSIvH0G+ttdHmLE68yhtWb2ZAnyXCHw9VhrEaNDMvR3yz7+KvJQF6JjWHybtrCe+AU5thCDIMGzEGkQFR0h3qbhllTFmJq7ujRFeUgIA8NDp0UTVAMs414MazwaQn5+PixcvSrcvX76MEydOwNfXF82aNcOcOXMwYcIEdOnSBTExMVi6dCkKCgqk2QFYRWUXBciXKqvifU6KmvUv6RneGHTIE0A6nt3yISK9umFh1DM4cnsbAGBe55V4tEN3OMkr72Tt7Sx2o7ghM/a3+/m313GEzprst/xGLoKEXMDVNr+lMVYnfm0gDvEn4MwP4rZDHYCBr0q7EBFI54bR8t1432m16eMv7hD/928PNO8BZYbxS7aqfP8dxhirJRcvP7yecgcHXJzxeG4+ZFG2uXR9jSurR44cQf/+/aXbc+bMAQBMmDABa9euxZgxY3Dz5k289tpryMjIQGRkJLZu3Vph0JW5rVy5EitXroSuzKTbtqLs1FW5xRoAKO2zKsBJUbOW1aY+rvBW+SIfgNw9Bad0KRhx2wfa0jb0vqGR96yoAoCnSqys6mQa/FszDv9y+ho3bh8DfJ1BJIM2tyP0hc3hh4/FB3CfVeaIBAEYtQbYVOZL+G3T+RTHdfJE62Mb8VD+5ns/T1BnAIBSbpyvWOlkmx8mjDHr4+Xrh0h1CSLVYuuqxs2yA75qq8bdAPr16ye2GJT7t3btWmmf5557DqmpqVCr1UhKSkK3bt3MWeZK2UM3gCKtFnnFWjj57Jcu2StrWFkFgFY+oSa3tTJxBgDSK+Hv7lHJI4y8SqeuUigLsFbfHznkijulI//9tPEQbjyClQUH4CToAKU74Fq9qbUYszvl1/HOSjG56Xvt96orqgAw8DUAgLPcOOhBFdbXHKVjjDEILqZT6DmpbHO6SaucDcDRyEpH5WflFuGJT/4HVcCv0n2+LhXnaryfpUNn4OHgFyr+Hr07ZLKqp8Vp6tEUIR4h0KMETw67hkz/3kh1EhvgW3v6Yr5iLQbJj4LkKuDhFQDPs8ocVfmrCrcvAiVlpqJSV7JAiVDmLXfEh9Ja4C28W8DfVfy5aaM2Zi4oY8xhlf9SXcOuhdaCK6tWwNCyCoEAmQaCIA6w+E9GFhq5+NX4+XxdPfB27ERse2ybyXYFqm5VBQCFTIE50WLXju8ufY6vfYpwrHTO1oTL72G0Yq9Y1Mc+BTo8UuOyMWY3yl+uJz1QkGW8rSmtuDbrYdzmWWZt1qYxxqeSOWHzw5uxKnYVEjtw/37GmJl4NjG9zZVVy7LlqatKtIYRFQRBJk5bIyNC96JioJrzrFYmyD0I09q+C+jcQXoFQpQ9q/W4gc0GItQzFADwveaKtN1LX2aUcrP6nd2BMavn01xstfBoAriJraLIvwl8+Siw6y1AUzpVlV9r42PKzsvauMx2AB5KD/QM7glXJ15ogzFmJp7BgFeZWXtsdIl0u6ms2kOfVQGEAJm4Io6bniDEPAv4hNXpuad2G4aj4/fhs747sX703OqVRxDwRo83KmxvV9pBGwoXwK1xncrFmM1TqIBZp4DnjwOls2jgyBrg0i5g3wdASYG4TeUB9JoDtBoMBEUZHy+zm7dfxpi1EgQgtLfxto123avxbADM/Pw8nAEtAEGPdvLzOALAVaYA4t4zy/MrFQp0a1GzgVDRAdHYNXoXHv4hDvk6NebdugPlo5+IrUEqD/EEYMzRqUq71hiWVLxeZoninNKln51cgf6viD9fOwr8MAmIfbPhysgYc2zhA4Hkr8Wfy35htiFcWbUCzgo5oAXkMgGe8mwAgKtMWfWDGoC/qz/+238VDn49HAMKC8XW1KBISxeLMetjuOR/65xx29kt4v9lL7s1jRZbYhljrKG0fxg491/APRDwCr7//lbIbiqrNj3PaukI4bayVCT53gYAuFlJvxKfoGgMlXkBcgICI+7/AMYcUdlKanncB5UxZklyJ3FeaBtmN52mbLnPqqz0MOhRjAtu4gCrYsFKDo0gAFP/Bzx/DHDjOVUZq1RwF/F/mQKYdhCITDDeZ6N9xBhjzFpYSY3IsRnmWf3ayzi1lLOVtKwCAFx9Ac8gS5eCMes16jNgwDzgxcuAfzsgxDgtlWEuVcYYY7VjN90AbFll3xhebp1QyVbGmFXyCQX6/MN4O+IJcd5VmQIIH2SxYjHGmD3gyqoVEApuAmUG17vp9egUzPOYMmazFCqgy1OWLgVjjNkFu+kGYMuLAhSDTG4XyGSAe4CFSsMYY4wxZj3sprJqywOs/qJik9seOr1xknHGGGOMMQdmN5VVW6Ym0+m2VjWNs1BJGGOMMcasC/dZtQJ3oYeh0+qWET+ihXdLyxaIMcYYY8xKcMuqFfAps5BBC59wXsqUMcYYY6wUV1atwAK/nuhdWIQvNT6WLgpjjDHGmFWxm24Atrzcaou4Zfgo5Weg1UOWLgpjjDHGmFURiIjuv5vtyM3NhZeXF3JycuDpySPqGWOMMcasTU3qa9wNgDHGGGOMWS2urDLGGGOMMavFlVXGGGOMMWa1uLLKGGOMMcasFldWGWOMMcaY1bKbyurKlSvRvn17dO3a1dJFYYwxxhhjZsJTVzHGGGOMsQbFU1cxxhhjjDG7wJVVxhhjjDFmtexmuVUDQ6+G3NxcC5eEMcYYY4xVxlBPq05vVLurrObl5QEAQkJCLFwSxhhjjDFWlby8PHh5eVW5j90NsNLr9bhx4wY8PDwgCEKtnyc3NxchISG4evWqww7UcuS/gSNnN3Dkv4EjZzdw5L+BI2cHOD/nb5j8RIS8vDwEBQVBJqu6V6rdtazKZDI0bdrUbM/n6enpkC/Wshz5b+DI2Q0c+W/gyNkNHPlv4MjZAc7P+es///1aVA14gBVjjDHGGLNaXFlljDHGGGNWiyur96BSqfD6669DpVJZuigW48h/A0fObuDIfwNHzm7gyH8DR84OcH7Ob3357W6AFWOMMcYYsx/cssoYY4wxxqwWV1YZY4wxxpjV4soqY4wxxhizWlxZZYwxxhhjVsuhK6tXr16FTqezdDGYheTn51u6CMyCbt68Wa01qZl94vPfsfHxty0OWVm9fPkyhg8fjieeeAI5OTkO94Gl1+sBwGEr6qmpqRg8eDD++c9/AjD+PRyFVqsF4Hi5Da5cuYK4uDhMmTIFgiA43N+Bz3/HPv/5+PPxB2zv+DtUZZWIMGXKFLRq1QqXLl3CkSNHAACCIFi4ZA1nzpw5+L//+z8AgFwut3BpGhYR4dlnn0V4eDgOHjyIPXv2QK/X33dNYnsyc+ZMDBs2DAAcKjdgPP6tWrXCyZMnsW/fPqjVaof6O/D579jnPx9/Pv62evwd5ii9//778Pb2xokTJ3Do0CF8++23CA0Nxf79+y1dtAZx/PhxDBo0COvXr8eGDRuwbds2ALb37aq2Fi9eLB3/Y8eO4Z133oGTkxMyMzMtXbQGkZKSgmHDhmHLli3YsWMHvvrqKwCO06qwaNEi6fgfPnwYq1atgp+fH06fPm3pojUIPv8d+/zn48/H39aPv8NUVvfv348lS5bg4MGDiIqKgru7O27cuCF9WNv7h/bhw4cRHByMtWvXYty4cZg7dy4A8duVvXeDuHDhArZs2YJly5YhKSkJHTt2RMeOHZGcnCydrPb+N0hJSUGTJk3w+eefY+bMmZg7dy40Go1DtCoUFBRgx44dWLp0KZKSkhAZGYlmzZrh/Pnz0nHn899+8fnPx5+Pvx0cf7JTGo3G5LZer5d+1mq1REQUFRVFM2fObMhiWUxGRgadPHmSiIj++OMPatKkCS1evJiIjH8Pe6VWq02Ov16vp+TkZGrZsiWtW7fOgiWrPzqdzuT2rVu36OzZs0REdPnyZQoKCqKXXnqp0n3tQflMZY+/Tqej27dvU9u2bendd99t6KJZBJ//jnX+l8fHn4+/rR9/haUry/Xhtddew+nTpxEcHIxp06ahdevWkMvl0Ol0kMvlkMvlKCwsRNOmTXH37l2o1WqrWgO3rhYsWICsrCy0bdsWiYmJUCqVCAgIQEBAAAAgMjISEyZMwMKFCzFp0iR4eHjYVd+dyvIDkDIKggA/Pz+o1Wqo1WoA4jdre+m7PH/+fFy+fBktWrTAtGnT0KhRI+kfAISEhODll1/GCy+8gKlTp6JZs2Z2n18QBOn8N7zOXV1d7XJEMJ//jn3+8/Hn42+Px9+6S1dDN2/eRK9evbB582ZERERg+/bteOKJJ7B8+XIAxgElRARXV1cEBgbi/PnzUKlUttMUXoVz586hQ4cO+Oabb5Ceno6XX34ZgwcPRlJSEgDjpQ5vb2+MGTMGfn5+0uUAe3C//Ibjr9fr0aRJE4SGhuLPP/+0ZJHN6urVq4iOjsamTZvg5uaGjz76CEOGDMGmTZsAGI+/XC7H2LFj0alTJ8ycOROAfQwyvF/+ssff19cXTZs2xbFjxwDYx2VAPv8d+/zn48/H366Pv2UadOvHTz/9RO3ataO0tDQiIiouLqZZs2ZRWFgY7d+/n4jEJm/DJYH169dTYGAgXbt2zWJlNqdFixZR9+7dpS4Q6enpFBERQY8//jhdvHiRiIzdI4qLi2nFihXk4eFBZ86cISKi3bt30507dyxTeDOoTn7D5WG1Wk1PPfUUxcXFUV5ensXKbE5r166lyMhIys7OJiKi/Px8GjFiBPXq1YtOnDhBRKbdY37++WcSBIH27NlDRETbtm2jc+fONXzBzaQ6+cte8po/fz5FRkbSzZs3LVJec+Pz37HPfz7+fPzt+fjbVctqVlYW8vPzpeZulUqFKVOm4IEHHjDpUGygUCjg6uqKrKwsi5TXnLRaLc6cOQN/f38pY2BgIP71r38hLS0Nn332GQAxMxFBpVIhLi4OvXr1QkJCAnr16oW4uDib/VtUN79MJoNer4dSqUTjxo2Rnp4Od3d3u2hZu3LlCpycnODm5gYAcHNzwwsvvACVSoWFCxcCMB5/ABg4cCDGjBmDCRMm4MEHH8TIkSORnZ1tqeLXWXXylx1Q4OHhgaKiIuh0Ops//nz+O/b5z8efj7+9H3+7qqyWlJQgICAAycnJ0rY2bdogMTER169fx8aNGwEYp2uIjY3F5cuXbfoD2kChUECtVqOoqAh6vV7KOHr0aERHRyMpKQnHjx8HYLwcoNVqcefOHSQnJ6Nt27bIyMhAmzZtLJahLmqS3zDye+DAgUhOTsalS5fs4jJ4cXExFAqFyRtOnz59MHToUKSkpGDnzp0AjMf/+vXruH37NlJTU9GxY0dkZmYiJibGImU3h+rmN7w2hgwZgvPnzyMzM9Pmjz+f/459/vPx5+Nv78ffpiqr9/r2Y9g+bNgw/P333/jf//4HjUYj3R8dHY3IyEjs2rULRASFQhxXlp+fj+effx7h4eE2/c3K8MKcNGkSdu7ciVOnTkEul0srFY0ePRppaWm4ePEiAPHb5ZEjRxAfHw+1Wo3Tp0/j008/hYeHh8Uy1EVN8xuOf15eHhITE+Ht7W3Tx9/w5jthwgQcPHgQhw4dMrk/NjYWKpUKR48eBSAe/3PnzmHcuHG4ceMGTp06hU8++cRmj39N8xuOf3Z2Np555hn4+/vb9PHn89+xz38+/nz8AQc4/g3Y5aBOcnNzK0w/YVC2H9706dOpefPmdPz4cZPHP/roozR27Fgiss2pegoLC+95nyF/UVER9e3bl2JjY4nI9G/UsmVLmj9/vnT71q1b9Oeff9ZTac3PnPkN/RbL3m8rKitz2df/6NGjqXPnzhX6YXbr1o1mzJgh3c7NzZX6cdoSc+S3xfO//FR8ld1nz+e/OfPb4vlfvl9lZZ9/9nz8zZnfFo//lStX6OrVq0RUcaopRzj+RDbQZ1Wj0WDKlCmIi4vDqFGjsG7dOgDi6GXDNweFQoHi4mIcP34cy5Ytg06nw4oVK5CammryXN7e3gBsa5lJjUaDqVOn4tFHH8X48eNx8OBB6VtgSUkJADG/TqdDTk4O3nzzTezZswerVq2S9rt79y7c3Nzg6+sLQGyJbtSoEXr27GmZUDVQH/kNfXps4dKPRqPBBx98gB9//BGAaZkN36gVCgVKSkpw8eJFfPDBB/jrr7+wZMkS5OTkABAv96hUKvj4+EiP9fDwQERERAMmqZ36yG9L539JSQlefPFFTJ48GXPmzMHff/8t3Vf2/c9ez//6yG9L539JSQlmzJiBkSNH4tFHH8WGDRukaZYMVw/t/fibO78tHX8A2LJlC8LCwjBjxgwAxvKXff+z1+NvwgIV5Gq7dOkSRUREUN++femnn36ixMREateuHU2ePNlkv2XLlpGHhwfNnTuXiIg2bdpEMTEx9MADD9Cnn35KM2fOpMaNG9POnTstEaPW0tPTqXPnztSjRw9auXIlRUREUERERIWJzJctW0ZKpZLWrl1LRERvv/02+fv706RJk2jv3r00e/ZsCgsLo5SUFEvEqDVHz//f//6X2rVrR4IgUEJCAl2/fp2IKrYILFu2jFxdXWnhwoVERLR69WoKDw+nwYMH05YtW2j27NnUpEkTOnToUINnqAtHz79x40YKCgqi/v3706uvvkpBQUE0aNAgaWYTA3t9/Tt6/nXr1lGTJk2oX79+tG7dOoqNjaXu3bvTb7/9ZrIf57fP/AavvPIKPfjggxQVFUWbNm0iItPWVXvPb2DVldUVK1ZQv379qKCggIjED6mPP/6YBEGg77//nnQ6Hb300kvk4+ND69evN7m8l5ycTAkJCTR48GDq3r07HThwwFIxam3Tpk3UoUMHaWqt7OxseuONN8jZ2ZlOnz5NRERjxoyhoKAg+uKLL0w+xJcvX069e/emjh07UkREBCUlJVkkQ104cv78/HyaNGkSPf/887RgwQLq0qULffzxxyb7qNVqmjJlCvn7+9OXX35p8vr/+eefKS4ujrp3705dunShgwcPNnSEOnH0/MePH6ehQ4fSggULpG1paWkUFhZGX3/9NRGJ50NCQoJdvv4dPf+5c+do1KhRtGTJEmnblStXKCAggHbs2EFEYv5x48ZxfjvMT2TsrjR9+nSaMWMGPf3009S7d28qKSkhIvt+/VfGqiurs2bNol69ehGRsTXlo48+IkEQqHPnznT79m3KysqinJwc6THlW13K3mcrDC/Sjz/+mIKCgkzuS09Pp4EDB1KfPn2IiOjgwYMmGct+YOt0Ovr7778boMTm5ej5icTX8f79++mvv/4iIqLHHnuMhg8fTsnJySb7nD9//p75icRl9myRo+dPSkqiF154QWpNNnxARUVF0bx584hI7KN26NAhu3z9O3r+O3fuUFJSEt29e1faduzYMXrooYfowIEDUj/FpKQkzm+H+Q30ej0NHjyYDh48SL/88gu1b9+eli1bRkRiZfXw4cOUm5sr7W9v+cuymsqqoeZf9o/96quvUmxsLP3666/StoSEBJo/fz6pVCqp2dtW1ratynfffUc7duygGzduSNtWr15NUVFRtHfvXpN9d+7cSU5OTrRt2zYiss0BI+Vx/or5y9q+fTt17tyZ3njjDZsaGFBdnF/Mb6icVSY7O5vatGlT4TKoPeD8Vb/+p0+fTgqFgiIjI6lx48Y0dOhQ2rdvHxHZ7+dfWY6Y35ArLi6O9u7dS7du3aJ58+ZRp06daNy4cbRgwQJSq9WWKnKDs3hl9ccff6SgoCDy9fWly5cvExFJB+Ds2bP0yCOPkJeXF40ZM4bc3d0pJiaGrl+/TmPHjqX4+HgLltw81q1bR/7+/hQTE0N+fn7Us2dPqV/KsWPHqH379vTuu++avCgzMjJoxIgR9OSTT1qq2GbD+Svm/+GHH4hIrISXrZhNmzaN+vbtK/W9todKG+e/d369Xm/yRSw1NZVatWolrUZjDzh/1a9/g7Fjx9LWrVspPz+f9u/fT48//jh1797dUsU2G85fMf+PP/4o3X/nzh0KDAyUPv9mz55Nzs7O5OLiQkeOHLFQqS3DopXV9evXU9euXWns2LHUq1cvevbZZ6X7DB9EaWlptGbNGpo+fTpt3rxZun/kyJH03HPPNXiZzUWj0dDSpUupXbt29Omnn5Jarab9+/fT+PHjaejQodJUTZMnT6aYmBj6448/TB7/2GOP0cSJEy1QcvPg/FXnLy4ulvY1vGmnpKRIUzDl5+eTTqeTlke1tdYFzl/9/Ib3wrVr11J4eLjJNG63b9822cdWcP7q5Tdc7i6fb968edS5c+cqW6KtGeevXv7r16/TmDFj6JtvvqGOHTtS48aNKT4+ntq2bUuHDx8mItt776sti8zhYphyITw8HAMHDsTChQsxYsQI7N69G7t37zbZJyQkBImJiVixYgUefvhhAEBGRgauXr2Kli1bWqL4ZlFQUICbN29iwoQJSExMhFKpRI8ePdC+fXvk5uZK0zK9+eab0Gg0WL16Na5fvy49vqioyGQqIlvD+avOb5iWBxCnWiIitG3bFo888giOHDmCt956C127dkVCQgJ0Op3JMsK2gPNXP79hip0tW7YgPj4eLi4uOHHiBB566CG89dZb0lQ+toTzVy+/YXnM8lO2Xbp0CdHR0QgKCrJUhDrh/FXnN0zLpdPpsHHjRowfPx59+vTBhQsXsHDhQoSGhmL27NkAYHPvfbXWkDXj8+fPV/iGZPjmdPr0aRoxYgTFxcVJ95Xf98qVK3Tt2jVKSEigzp07U2pqav0X2ozK5z9+/Lj0rcjQevTVV19RZGSkyWXv7777jnr37k3NmzenRYsW0ZNPPkn+/v5Snx1bwflrl7/s/YcPHyYnJycSBIEmT55sU32WOH/t8+fn59OAAQPom2++oalTp5JcLqeEhARp4JEt4Py1z08kLoxy7do1mjRpErVp00a62mQrrcqcv3b5v/322wqj+VetWkXvv/8+6fV6m8lfVw1SWd2wYQOFhoZSmzZtKCYmhj777DPpvrJ/6DVr1lD79u1pzZo1RGTaZ6WwsJDmzZtHvr6+1Lt3b5vqt1Q+/6effmpyf9mc48aNky5vl33BXrt2jSZPnkwjR46kuLg4aZS0LeD8tctfftUew7RtDz30EF26dKn+C24mnL/u+U+cOEGCIJAgCPTggw/S2bNnG6bwZsD5a5e/7OXd77//np5//nkKCAigfv360YULFxqm8GbA+WuXv7IvYob6kqNc+i+r3iur27dvp9DQUFq5ciVt3bqV5syZQ05OTrR69Wqp75HhTenatWv09NNPU9euXaXl1coesBMnTtCePXvqu8hmVVX+oqIiIiLp21FRURF16tSJvvzyy3s+n+ExtoLzmy9/cnIybdiwoSGLX2ec3zz59+7dS/369ZPmmLQVnN88+c+cOUMffPCBzS1sw/nNk98RK6fl1Vtl1fAN4M0336To6GiTSue0adOoS5cu0qi/sn755Rfq0qULvf7665ScnEzx8fGUlpZWX8WsN7XJf/36dQoNDaXz588TkXjZYPbs2Q1XaDPi/JyfiPPXNf+sWbMartBmxPk5PxHnd9T3v/pQbwOsDB2iz549i5YtW8LJyUnqNPz222/D2dkZW7ZsQUZGBgDjgKr+/fsjJiYG8+fPR3R0NDQaDfz9/eurmPWmpvkBYOfOnQgJCUGTJk0wc+ZMtG/fHqmpqdBoNNI6v7aC83N+gPPXNX9aWho0Gg30er1FctQW5zdvfkd//Ttqflt9/6sX5qr1bt++nWbMmEFLliwx6Qy8evVq8vDwkJqxDd8wVq9eTa1bt6bdu3dL++bn59OSJUtILpdTv3796OTJk+YqXr2rbf6yncRHjx5NPj4+1KhRI+rQoYM0NYUt4Pycn/Nzfs7P+Tm/4+VvCHWurN64cYPi4+PJ39+fEhISqGPHjuTl5SUdsHPnzlFwcDC9+uqrRGQ6aCYwMNBk7d8zZ85Qt27daN26dXUtVoMxV/6CggKKj4+npk2b0rffftvgOWqL83N+zs/5OT/n5/yOl78h1amyWlBQQBMmTKAxY8aYrEEbExMjjWjLzc2lt99+m1xcXKS+p4b+HH379qVJkybVpQgWZe78trYiBefn/Jyf83N+zs/5HS9/Q6tTn1VXV1eoVCpMnDgRYWFh0kS+cXFxSElJARHBw8MD48aNQ1RUFB5//HGkpqZCEASkpaUhKysLI0eONEdvBoswd/7o6GgLJakdzs/5OT/n5/ycn/M7Xv4GV9fabtlRbob5wsaNG0fPPPOMyX7Xrl2j8PBwCg0NpVGjRlFQUBANGDCAMjIy6loEi+L8nN+A83N+Is7P+Tk/53eM/A1JIDL/MLNevXrhmWeewYQJE6RRnDKZDBcvXsTRo0eRlJSEiIgITJgwwdy/2ipwfs7P+Tk/5+f8nJ/zA46Vv96Yu/Z76dIlCggIMOl/YUtLItYV5+f8nJ/zc37Ob8D5OT+rO7PNs0qlDbR//vkn3N3dpf4Xb775JmbOnImsrCxz/SqrxPk5P8D5OT/n5/ycn/M7Vv6GoDDXExkmwT106BAee+wx7NixA5MnT0ZhYSG+/PJLm5zYvyY4P+cHOD/n5/ycn/NzfsfK3yDM2UxbVFRE4eHhJAgCqVQqevfdd8359FaP83N+zs/5OT/n5/yc39Hy1zezD7AaNGgQWrVqhcWLF8PZ2dmcT20TOD/n5/ycn/Nzfs7P+Zn5mL2yqtPpIJfLzfmUNoXzc37Oz/kdFefn/JzfcfPXp3qZuooxxhhjjDFzMNtsAIwxxhhjjJkbV1YZY4wxxpjV4soqY4wxxhizWlxZZYwxxhhjVosrq4wxxhhjzGpxZZUxxhhjjFktrqwyxhhjjDGrxZVVxhhrIBMnToQgCBAEAU5OTggICMCgQYOwZs0a6PX6aj/P2rVr4e3tXX8FZYwxK8KVVcYYa0BDhgxBeno6rly5gt9++w39+/fHzJkzER8fD61Wa+niMcaY1eHKKmOMNSCVSoXAwEAEBwcjKioKr7zyCrZs2YLffvsNa9euBQAsXrwYHTt2hJubG0JCQjBt2jTk5+cDAHbv3o3ExETk5ORIrbRvvPEGAECtVmPu3LkIDg6Gm5sbunXrht27d1smKGOMmQlXVhljzMIGDBiAiIgI/PDDDwAAmUyG5cuX48yZM/jiiy/w+++/48UXXwQA9OjRA0uXLoWnpyfS09ORnp6OuXPnAgCee+45HDhwAN9++y1OnjyJ0aNHY8iQIbhw4YLFsjHGWF0JRESWLgRjjDmCiRMnIjs7G5s3b65w39ixY3Hy5EmcPXu2wn2bNm3ClClTcOvWLQBin9VZs2YhOztb2ictLQ0tWrRAWloagoKCpO2xsbGIiYnBO++8Y/Y8jDHWEBSWLgBjjDGAiCAIAgBg586dWLBgAf766y/k5uZCq9WiuLgYhYWFcHV1rfTxp06dgk6nQ+vWrU22q9VqNGrUqN7Lzxhj9YUrq4wxZgVSUlIQFhaGK1euID4+HlOnTsW///1v+Pr64s8//8TTTz+NkpKSe1ZW8/PzIZfLcfToUcjlcpP73N3dGyICY4zVC66sMsaYhf3+++84deoUZs+ejaNHj0Kv12PRokWQycRhBRs3bjTZX6lUQqfTmWzr3LkzdDodsrKy0Lt37wYrO2OM1TeurDLGWANSq9XIyMiATqdDZmYmtm7digULFiA+Ph7jx4/H6dOnodFo8OGHH2L48OHYv38/Vq1aZfIcoaGhyM/Px65duxAREQFXV1e0bt0aCQkJGD9+PBYtWoTOnTvj5s2b2LVrFzp16oRhw4ZZKDFjjNUNzwbAGGMNaOvWrWjSpAlCQ0MxZMgQ/PHHH1i+fDm2bNkCuVyOiIgILF68GAsXLsQDDzyAr776CgsWLDB5jh49emDKlCkYM2YM/Pz88N577wEAPv/8c4wfPx4vvPAC2rRpg5EjR+Lw4cNo1qyZJaIyxphZ8GwAjDHGGGPManHLKmOMMcYYs1pcWWWMMcYYY1aLK6uMMcYYY8xqcWWVMcYYY4xZLa6sMsYYY4wxq8WVVcYYY4wxZrW4ssoYY4wxxqwWV1YZY4wxxpjV4soqY4wxxhizWlxZZYwxxhhjVosrq4wxxhhjzGpxZZUxxhhjjFmt/wdlUsiuaAksbAAAAABJRU5ErkJggg==", 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 | --------------------------------------------------------------------------------