From 4b5d5d428faed82b21392d7eec93bb9bd38f5767 Mon Sep 17 00:00:00 2001 From: Gavin Chan Date: Wed, 21 Jun 2023 22:53:02 +0100 Subject: [PATCH] feat: add covariance estimator --- .../crypto_statistical_risk_model.ipynb | 263 +++++++++--------- src/fpm_risk_model/cov_estimator.py | 99 +++++++ src/fpm_risk_model/risk_model.py | 19 +- tests/test_cov_estimator.py | 137 +++++++++ 4 files changed, 377 insertions(+), 141 deletions(-) create mode 100644 src/fpm_risk_model/cov_estimator.py create mode 100644 tests/test_cov_estimator.py diff --git a/examples/notebook/crypto_statistical_risk_model.ipynb b/examples/notebook/crypto_statistical_risk_model.ipynb index 8ced840..4f686d0 100644 --- a/examples/notebook/crypto_statistical_risk_model.ipynb +++ b/examples/notebook/crypto_statistical_risk_model.ipynb @@ -54,28 +54,30 @@ "name": "stdout", "output_type": "stream", "text": [ - "Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n", - "Requirement already satisfied: matplotlib in /usr/local/lib/python3.10/dist-packages (3.7.1)\n", - "Requirement already satisfied: factor-pricing-model-risk-model in /usr/local/lib/python3.10/dist-packages (2023.4.0)\n", - "Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib) (1.0.7)\n", - "Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.10/dist-packages (from matplotlib) (0.11.0)\n", - "Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.10/dist-packages (from matplotlib) (4.39.3)\n", - "Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib) (1.4.4)\n", - "Requirement already satisfied: numpy>=1.20 in /usr/local/lib/python3.10/dist-packages (from matplotlib) (1.22.4)\n", - "Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.10/dist-packages (from matplotlib) (23.1)\n", - "Requirement already satisfied: pillow>=6.2.0 in /usr/local/lib/python3.10/dist-packages (from matplotlib) (8.4.0)\n", - "Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib) (3.0.9)\n", - "Requirement already satisfied: python-dateutil>=2.7 in /usr/local/lib/python3.10/dist-packages (from matplotlib) (2.8.2)\n", - "Requirement already satisfied: pandas<1.4.0,>=1.3.5 in /usr/local/lib/python3.10/dist-packages (from factor-pricing-model-risk-model) (1.3.5)\n", - "Requirement already satisfied: pydantic<2.0.0,>=1.10.4 in /usr/local/lib/python3.10/dist-packages (from factor-pricing-model-risk-model) (1.10.7)\n", - "Requirement already satisfied: scikit-learn<2.0.0,>=1.1.3 in /usr/local/lib/python3.10/dist-packages (from factor-pricing-model-risk-model) (1.2.2)\n", - "Requirement already satisfied: tqdm<5.0.0,>=4.64.1 in /usr/local/lib/python3.10/dist-packages (from factor-pricing-model-risk-model) (4.65.0)\n", - "Requirement already satisfied: pytz>=2017.3 in /usr/local/lib/python3.10/dist-packages (from pandas<1.4.0,>=1.3.5->factor-pricing-model-risk-model) (2022.7.1)\n", - "Requirement already satisfied: typing-extensions>=4.2.0 in /usr/local/lib/python3.10/dist-packages (from pydantic<2.0.0,>=1.10.4->factor-pricing-model-risk-model) (4.5.0)\n", - "Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.10/dist-packages (from python-dateutil>=2.7->matplotlib) (1.16.0)\n", - "Requirement already satisfied: scipy>=1.3.2 in /usr/local/lib/python3.10/dist-packages (from scikit-learn<2.0.0,>=1.1.3->factor-pricing-model-risk-model) (1.10.1)\n", - "Requirement already satisfied: joblib>=1.1.1 in /usr/local/lib/python3.10/dist-packages (from scikit-learn<2.0.0,>=1.1.3->factor-pricing-model-risk-model) (1.2.0)\n", - "Requirement already satisfied: threadpoolctl>=2.0.0 in /usr/local/lib/python3.10/dist-packages (from scikit-learn<2.0.0,>=1.1.3->factor-pricing-model-risk-model) (3.1.0)\n" + "Requirement already satisfied: matplotlib in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (3.7.1)\n", + "Requirement already satisfied: factor-pricing-model-risk-model in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (2023.3.0)\n", + "Requirement already satisfied: pyparsing>=2.3.1 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from matplotlib) (3.0.9)\n", + "Requirement already satisfied: kiwisolver>=1.0.1 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from matplotlib) (1.4.4)\n", + "Requirement already satisfied: numpy>=1.20 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from matplotlib) (1.23.5)\n", + "Requirement already satisfied: packaging>=20.0 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from matplotlib) (22.0)\n", + "Requirement already satisfied: contourpy>=1.0.1 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from matplotlib) (1.0.7)\n", + "Requirement already satisfied: fonttools>=4.22.0 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from matplotlib) (4.39.3)\n", + "Requirement already satisfied: python-dateutil>=2.7 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from matplotlib) (2.8.2)\n", + "Requirement already satisfied: pillow>=6.2.0 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from matplotlib) (9.5.0)\n", + "Requirement already satisfied: cycler>=0.10 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from matplotlib) (0.11.0)\n", + "Requirement already satisfied: scikit-learn<2.0.0,>=1.1.3 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from factor-pricing-model-risk-model) (1.2.0)\n", + "Requirement already satisfied: pandas<1.4.0,>=1.3.5 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from factor-pricing-model-risk-model) (1.3.5)\n", + "Requirement already satisfied: pydantic<2.0.0,>=1.10.4 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from factor-pricing-model-risk-model) (1.10.4)\n", + "Requirement already satisfied: tqdm<5.0.0,>=4.64.1 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from factor-pricing-model-risk-model) (4.64.1)\n", + "Requirement already satisfied: pytz>=2017.3 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from pandas<1.4.0,>=1.3.5->factor-pricing-model-risk-model) (2022.6)\n", + "Requirement already satisfied: typing-extensions>=4.2.0 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from pydantic<2.0.0,>=1.10.4->factor-pricing-model-risk-model) (4.4.0)\n", + "Requirement already satisfied: six>=1.5 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from python-dateutil>=2.7->matplotlib) (1.16.0)\n", + "Requirement already satisfied: joblib>=1.1.1 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from scikit-learn<2.0.0,>=1.1.3->factor-pricing-model-risk-model) (1.2.0)\n", + "Requirement already satisfied: threadpoolctl>=2.0.0 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from scikit-learn<2.0.0,>=1.1.3->factor-pricing-model-risk-model) (3.1.0)\n", + "Requirement already satisfied: scipy>=1.3.2 in /Users/gavinchan/workspace/factor-pricing-model-risk-model/.env/lib/python3.10/site-packages (from scikit-learn<2.0.0,>=1.1.3->factor-pricing-model-risk-model) (1.9.3)\n", + "\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.0.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m23.1.2\u001b[0m\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n" ] } ], @@ -83,6 +85,16 @@ "!pip install matplotlib factor-pricing-model-risk-model" ] }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, { "cell_type": "code", "execution_count": 2, @@ -99,6 +111,7 @@ }, "outputs": [], "source": [ + "from functools import partial\n", "import pandas as pd\n", "\n", "from fpm_risk_model.dataset.crypto import (\n", @@ -307,12 +320,14 @@ { "name": "stderr", "output_type": "stream", - "text": [] + "text": [ + " \r" + ] }, { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 6, @@ -358,10 +373,7 @@ { "data": { "text/html": [ - "\n", - "
\n", - "
\n", - "
\n", + "
\n", "\n", - "\n", - " \n", - "
\n", - "
\n", - " " + "
" ], "text/plain": [ " bitcoin ethereum\n", @@ -642,12 +578,14 @@ { "name": "stderr", "output_type": "stream", - "text": [] + "text": [ + " \r" + ] }, { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 10, @@ -706,6 +644,20 @@ "tags": [] }, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> \u001b[0;32m/Users/gavinchan/workspace/factor-pricing-model-risk-model/src/fpm_risk_model/accuracy/bias.py\u001b[0m(55)\u001b[0;36mcompute_standardized_returns\u001b[0;34m()\u001b[0m\n", + "\u001b[0;32m 53 \u001b[0;31m \u001b[0;32mif\u001b[0m \u001b[0mindex\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mlist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrolling_risk_model\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkeys\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0m\u001b[0;32m 54 \u001b[0;31m \u001b[0;32mimport\u001b[0m \u001b[0mpdb\u001b[0m\u001b[0;34m;\u001b[0m \u001b[0mpdb\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mset_trace\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0m\u001b[0;32m---> 55 \u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0m\u001b[0;32m 56 \u001b[0;31m \u001b[0;32mif\u001b[0m \u001b[0mrisk_model\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0m\u001b[0;32m 57 \u001b[0;31m \u001b[0;32mcontinue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0m\n", + "1\n" + ] + }, { "data": { "text/plain": [ @@ -802,6 +754,44 @@ "var_breach_statistics.plot()" ] }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Covariance Estimator\n", + "\n", + "Covariance estimator is another way to retrieve covariances from the factor models\n", + "with advanced techniques.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [], + "source": [ + "from fpm_risk_model.cov_estimator import RollingCovarianceEstimator\n", + "ewma_vol = model_universe_returns.ewm(halflife=63, min_periods=30).std()\n", + "pca_ewma_adj_vol_covs = (\n", + " RollingCovarianceEstimator(rolling_risk_model)\n", + " .cov(volatility=ewma_vol)\n", + ")\n", + "pca_ewma_adj_vol_covs_bias_statistics = compute_bias_statistics(\n", + " X=model_universe_returns.where(model_universe_returns.abs() < 0.2),\n", + " weights=weights,\n", + " window=63,\n", + " rolling_risk_model=pca_ewma_adj_vol_covs,\n", + ")\n", + "pca_ewma_adj_vol_covs_var_breach_statistics = compute_value_at_risk_rolling_breach_statistics(\n", + " X=model_universe_returns.where(model_universe_returns.abs() < 0.2),\n", + " weights=weights,\n", + " window=63,\n", + " rolling_risk_model=pca_ewma_adj_vol_covs,\n", + ")" + ] + }, { "attachments": {}, "cell_type": "markdown", @@ -819,7 +809,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -832,7 +822,19 @@ { "name": "stderr", "output_type": "stream", - "text": [] + "text": [ + " \r" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" }, { "data": { @@ -883,36 +885,21 @@ " weights=weights,\n", " window=63,\n", " rolling_risk_model=rolling_apca_risk_model,\n", - ")\n", - "apca_bias_statistics = compute_bias_statistics(\n", - " X=model_universe_returns.where(model_universe_returns.abs() < 0.2),\n", - " weights=weights,\n", - " window=63,\n", - " rolling_risk_model=rolling_apca_risk_model,\n", - ")\n", - "apca_var_breach_statistics = compute_value_at_risk_rolling_breach_statistics(\n", - " X=model_universe_returns.where(model_universe_returns.abs() < 0.2),\n", - " weights=weights,\n", - " window=63,\n", - " rolling_risk_model=rolling_apca_risk_model,\n", - ")\n", - "\n", - "bias_statistics_comparison = pd.concat({\n", - " '1. PCA': bias_statistics,\n", - " '2. APCA': apca_bias_statistics\n", - "}, axis=1)\n", - "\n", - "var_breach_comparison = pd.concat({\n", - " '1. PCA': var_breach_statistics,\n", - " '2. APCA': apca_var_breach_statistics\n", - "}, axis=1)\n", - "\n", - "bias_statistics_comparison = pd.concat({\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pd.concat({\n", " '1. PCA': bias_statistics,\n", " '2. APCA': apca_bias_statistics\n", "}, axis=1).plot(title=\"Bias statistics comparison\")\n", "\n", - "var_breach_comparison = pd.concat({\n", + "pd.concat({\n", " '1. PCA': var_breach_statistics,\n", " '2. APCA': apca_var_breach_statistics\n", "}, axis=1).plot(title=\"VaR Breach comparison\")" @@ -930,10 +917,8 @@ }, { "cell_type": "code", - "execution_count": 13, - "metadata": { - "id": "PH5mzfcKjvwN" - }, + "execution_count": null, + "metadata": {}, "outputs": [], "source": [] } diff --git a/src/fpm_risk_model/cov_estimator.py b/src/fpm_risk_model/cov_estimator.py new file mode 100644 index 0000000..db2b670 --- /dev/null +++ b/src/fpm_risk_model/cov_estimator.py @@ -0,0 +1,99 @@ +from typing import Optional + +from pandas import DataFrame, Series + +from .risk_model import RiskModel +from .rolling_factor_risk_model import RollingFactorRiskModel + + +class CovarianceEstimator: + """ + Covariance estimator. + """ + + def __init__(self, risk_model: RiskModel): + """ + Constructor. + + Parameters + ---------- + risk_model : RiskModel + Risk model object. + """ + self._risk_model = risk_model + + def corr(self) -> DataFrame: + """ + Correlation + """ + return self._risk_model.corr() + + def cov( + self, volatility: Optional[Series] = None, strict: bool = True + ) -> DataFrame: + """ + Correlation + + Parameters + ---------- + volatility : pd.Series + Volaility series to convert from correlation to covariance. Optional. + strict : bool + Indicates to throw exception if volatility series does not align with + correlation matrix. + """ + if volatility is None: + return self._risk_model.cov() + + corr = self._risk_model.corr() + if strict and set(volatility.index) != set(corr.index): + raise ValueError( + "Incorrect volatility series passed. Length of volatility " + f"is {len(volatility.index)} while that of correlation is " + f"{len(corr.index)}" + ) + elif len(volatility.index) < len(corr.index): + instruments = volatility.index + else: + instruments = corr.index + + volatility = volatility.loc[instruments] + cov = ( + corr.loc[instruments, instruments] + .mul(volatility, axis=0) + .mul(volatility, axis=1) + ) + return cov + + +class RollingCovarianceEstimator: + """ + Rolling covariance estimator. + """ + + def __init__(self, rolling_risk_model: RollingFactorRiskModel): + """ + Constructor. + + Parameters + ---------- + rolling_risk_model : RollingFactorRiskModel + Rolling risk model object. + """ + self._rolling_risk_model = rolling_risk_model + + def cov(self, volatility: Optional[DataFrame] = None): + """ + Correlation + + Parameters + ---------- + volatility : DataFrame + Volaility series to convert from correlation to covariance. Optional. + """ + return { + date: CovarianceEstimator(risk_model).cov( + volatility.loc[date], strict=False + ) + for date, risk_model in self._rolling_risk_model.items() + } diff --git a/src/fpm_risk_model/risk_model.py b/src/fpm_risk_model/risk_model.py index 2beabcd..2bad22a 100644 --- a/src/fpm_risk_model/risk_model.py +++ b/src/fpm_risk_model/risk_model.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod from typing import Any, Union -from numpy import ndarray +from numpy import diagonal, ndarray, sqrt from pandas import DataFrame, Series from .config import Config @@ -73,7 +73,22 @@ def cov(self, **kwargs) -> ndarray: diagonal entries are the variances. """ - def corr(self, **kwargs): + def vol(self, **kwargs) -> ndarray: + """ + Get the volatility series. + + Returns + ------- + numpy.ndarray + Volatility series derived from covariance matrix. + """ + cov = self.cov(**kwargs) + vol = sqrt(diagonal(cov)) + if isinstance(cov, DataFrame): + vol = Series(vol, index=cov.index) + return vol + + def corr(self, **kwargs) -> ndarray: """ Get the correlation matrix. diff --git a/tests/test_cov_estimator.py b/tests/test_cov_estimator.py new file mode 100644 index 0000000..ba30ef5 --- /dev/null +++ b/tests/test_cov_estimator.py @@ -0,0 +1,137 @@ +import pytest +from numpy import array +from pandas import DataFrame, Series, bdate_range +from pandas.testing import assert_frame_equal + +from fpm_risk_model.cov_estimator import CovarianceEstimator +from fpm_risk_model.factor_risk_model import FactorRiskModel + + +@pytest.fixture(scope="module") +def instruments(): + return ["A", "AAL", "AAP", "AAPL"] + + +@pytest.fixture(scope="module") +def valid_instruments(): + return ["A", "AAL", "AAPL"] + + +@pytest.fixture(scope="module") +def dates(): + return bdate_range("2016-01-04", "2016-01-15") + + +@pytest.fixture(scope="module") +def factors(): + return ["factor_1", "factor_2"] + + +@pytest.fixture(scope="module") +def factor_exposures(): + return array( + [ + [-0.15454215, -0.22795166, 0.0, -0.17179763], + [0.00706732, 0.08354979, 0.0, -0.11721647], + ] + ) + + +@pytest.fixture(scope="module") +def factor_returns(): + return array( + [ + [0.06323026, -0.15644581], + [0.01829957, 0.09617634], + [-0.06074615, 0.17670851], + [0.1238173, 0.14189995], + [-0.03715707, -0.04710242], + [-0.08798148, -0.03209175], + [-0.1300186, 0.00469255], + [0.14264733, -0.05445549], + [-0.13802269, -0.07709159], + [0.10593153, -0.05229029], + ] + ) + + +@pytest.fixture(scope="module") +def residual_returns(): + return array( + [ + [-0.00422976, 0.00199051, 0.0, 0.00116378], + [0.01038799, -0.00488857, 0.0, -0.00285816], + [0.00548287, -0.00258023, 0.0, -0.00150856], + [-0.01266259, 0.00595899, 0.0, 0.003484], + [-0.00424175, 0.00199616, 0.0, 0.00116708], + [-0.01853336, 0.00872176, 0.0, 0.00509929], + [-0.00185692, 0.00087386, 0.0, 0.00051091], + [-0.00071556, 0.00033674, 0.0, 0.00019688], + [0.01124235, -0.00529063, 0.0, -0.00309323], + [0.01512673, -0.00711861, 0.0, -0.00416198], + ] + ) + + +@pytest.fixture(scope="module") +def factor_risk_model_np(factor_exposures, factor_returns, residual_returns): + return FactorRiskModel( + factor_exposures=factor_exposures, + factor_returns=factor_returns, + residual_returns=residual_returns, + ) + + +@pytest.fixture(scope="module") +def factor_risk_model( + factor_exposures, + factor_returns, + residual_returns, + dates, + instruments, + factors, +): + return FactorRiskModel( + factor_exposures=DataFrame( + factor_exposures, index=factors, columns=instruments + ), + factor_returns=DataFrame(factor_returns, index=dates, columns=factors), + residual_returns=DataFrame(residual_returns, index=dates, columns=instruments), + ) + + +def test_cov_estimator_no_adj(factor_risk_model): + cov_estimator = CovarianceEstimator(factor_risk_model) + cov = cov_estimator.cov() + target_cov = DataFrame( + [ + [0.00038113, 0.00039798, 0.0002858], + [0.00039798, 0.00068043, 0.00032631], + [0.0002858, 0.00032631, 0.00048932], + ], + index=cov.index, + columns=cov.columns, + ) + assert_frame_equal( + cov, + target_cov, + ) + + +def test_cov_estimator_vol_adj(factor_risk_model, instruments): + cov_estimator = CovarianceEstimator(factor_risk_model) + vol = Series(0.2, index=instruments) + cov = cov_estimator.cov(volatility=vol, strict=False) + target_cov = DataFrame( + [ + [0.04, 0.03126062, 0.02647162], + [0.03126062, 0.04, 0.02262061], + [0.02647162, 0.02262061, 0.04], + ], + index=cov.index, + columns=cov.columns, + ) + assert_frame_equal( + cov, + target_cov, + )