Skip to content

Commit

Permalink
feat: allow passing dict of covariances in accuracy functions
Browse files Browse the repository at this point in the history
  • Loading branch information
gavincyi committed Jun 20, 2023
1 parent f9a72a9 commit 3f2452d
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 29 deletions.
34 changes: 34 additions & 0 deletions docs/source/accuracy/bias.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,40 @@ Assuming the portfolio returns are normally distributed, the 95% confidence
bounds for $B_T(t)$ are between $1-\sqrt{\frac{2}{T}}$ and
$1+\sqrt{\frac{2}{T}}$.

## Usage

Assume that you have market returns `returns` in a `DataFrame`, portfolio
weights `weights` in a `DataFrame` (it could be equally weighted, or market
cap weighted), and rolling risk models, which can be a `RollingRiskModel`
object, or just a dictionary of covariances. To compute the bias statistic
in a 21-day rolling window, you can use the following code snippet.

```
from fpm_risk_model.accuracy.bias import compute_bias_statistics
compute_bias_statistics(
X=returns,
weights=weights,
rolling_risk_model=rolling_risk_model,
window=30,
)
```

In the meantime, if you have the forecast portfolio volatility, named
`forecast_vols`, from vendor, you can directly pass it into the function
as well.

```
compute_bias_statistics(
X=returns,
weights=weights,
forecast_vols=forecast_vols,
window=30,
)
```

Please refer to the below section for more information.

## Reference

Alexander, Carol (2009). Market risk analysis, value at risk models. John Wiley & Sons.
Expand Down
36 changes: 36 additions & 0 deletions docs/source/accuracy/value_at_risk.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,42 @@ The expected rolling-window VaR breach statistics should be around
- overestimates the risk if it is below the expected percentage, and
- underestimates the risk otherwise

## Usage

Assume that you have market returns `returns` in a `DataFrame`, portfolio
weights `weights` in a `DataFrame` (it could be equally weighted, or market
cap weighted), and rolling risk models, which can be a `RollingRiskModel`
object, or just a dictionary of covariances. To compute the bias statistic
in a 21-day rolling window, you can use the following code snippet.

```
from fpm_risk_model.accuracy.value_at_risk import (
compute_value_at_risk_breach_statistics
)
compute_value_at_risk_breach_statistics(
X=returns,
weights=weights,
rolling_risk_model=rolling_risk_model,
window=30,
)
```

In the meantime, if you have the forecast portfolio volatility, named
`forecast_vols`, from vendor, you can directly pass it into the function
as well.

```
compute_value_at_risk_breach_statistics(
X=returns,
weights=weights,
forecast_vols=forecast_vols,
window=30,
)
```

Please refer to the below section for more information.

## Reference

Alexander, Carol (2009). Market risk analysis, value at risk models. John Wiley & Sons.
Expand Down
27 changes: 14 additions & 13 deletions src/fpm_risk_model/accuracy/bias.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional
from typing import Any, Dict, Optional, Union

from numpy import nan, sqrt, sum
from pandas import DataFrame, Series
Expand All @@ -9,7 +9,7 @@
def compute_standardized_returns(
X: DataFrame,
weights: DataFrame,
rolling_risk_model: Optional[RollingFactorRiskModel] = None,
rolling_risk_model: Optional[Union[RollingFactorRiskModel, Dict[Any, Any]]] = None,
forecast_vols: Optional[Series] = None,
cov_halflife: Optional[float] = None,
) -> Series:
Expand All @@ -28,8 +28,9 @@ def compute_standardized_returns(
The instrument forecast returns.
weights: ndarray
Weights of the instruments.
rolling_risk_model: RollingFactorRiskModel
The rolling risk model.
rolling_risk_model: Union[RollingFactorRiskModel, Dict[Any, Any]]
A rolling risk model object or dictionary of covariances of
which the keys and values are dates and covariances.
forecast_vols: Series
The forecast volatility.
cov_halflife: Optional[float]
Expand All @@ -50,12 +51,11 @@ def compute_standardized_returns(
risk_model = rolling_risk_model.get(index)
if risk_model is None:
continue
cov = (
risk_model.cov(halflife=cov_halflife)
.reindex(index=instruments, columns=instruments)
.fillna(0.0)
.values
)
elif isinstance(risk_model, DataFrame):
cov = risk_model
else:
cov = risk_model.cov(halflife=cov_halflife)
cov = cov.reindex(index=instruments, columns=instruments).fillna(0.0).values
vol = sqrt((cov @ index_weights) @ index_weights)
b_t[index] = returns / vol

Expand All @@ -66,7 +66,7 @@ def compute_bias_statistics(
X: DataFrame,
weights: DataFrame,
window: int,
rolling_risk_model: Optional[RollingFactorRiskModel] = None,
rolling_risk_model: Optional[Union[RollingFactorRiskModel, Dict[Any, Any]]] = None,
forecast_vols: Optional[Series] = None,
min_periods: Optional[int] = None,
cov_halflife: Optional[float] = None,
Expand All @@ -92,8 +92,9 @@ def compute_bias_statistics(
Weights of the instruments.
forecast_vols: Optional[Series]
The forecast volatility.
rolling_risk_model: RollingFactorRiskModel
The rolling risk model.
rolling_risk_model: Union[RollingFactorRiskModel, Dict[Any, Any]]
A rolling risk model object or dictionary of covariances of
which the keys and values are dates and covariances.
cov_halflife: Optional[float]
Halflife in computing covariances.
Expand Down
35 changes: 19 additions & 16 deletions src/fpm_risk_model/accuracy/value_at_risk.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional
from typing import Any, Dict, Optional, Union

from numpy import nan, ndarray, sqrt, sum
from pandas import DataFrame, Series
Expand All @@ -9,7 +9,7 @@

def compute_value_at_risk_threshold(
weights: DataFrame,
rolling_risk_model: Optional[RollingFactorRiskModel] = None,
rolling_risk_model: Optional[Union[RollingFactorRiskModel, Dict[Any, Any]]] = None,
forecast_vols: Optional[Series] = None,
threshold: Optional[float] = 0.95,
cov_halflife: Optional[float] = None,
Expand All @@ -25,8 +25,9 @@ def compute_value_at_risk_threshold(
respectively. The weights should be normalized, i.e.
sum to one for each time frame.
rolling_risk_model: Optional[RollingFactorRiskModel]
The rolling risk model.
rolling_risk_model: Union[RollingFactorRiskModel, Dict[Any, Any]]
A rolling risk model object or dictionary of covariances of
which the keys and values are dates and covariances.
forecast_vols: Optional[Series]
The forecast volatility of instruments.
Expand All @@ -48,12 +49,12 @@ def compute_value_at_risk_threshold(
risk_model = rolling_risk_model.get(index)
if risk_model is None:
continue
cov = (
risk_model.cov(halflife=cov_halflife)
.reindex(index=instruments, columns=instruments)
.fillna(0.0)
.values
)
elif isinstance(risk_model, DataFrame):
cov = risk_model
else:
cov = risk_model.cov(halflife=cov_halflife)

cov = cov.reindex(index=instruments, columns=instruments).fillna(0.0).values
vol = sqrt((cov @ index_weights) @ index_weights)
else:
vol = forecast_vols[index]
Expand All @@ -65,7 +66,7 @@ def compute_value_at_risk_threshold(
def compute_value_at_risk_breach_statistics(
X: DataFrame,
weights: DataFrame,
rolling_risk_model: Optional[RollingFactorRiskModel] = None,
rolling_risk_model: Optional[Union[RollingFactorRiskModel, Dict[Any, Any]]] = None,
forecast_vols: Optional[Series] = None,
threshold: Optional[float] = 0.95,
cov_halflife: Optional[float] = None,
Expand All @@ -85,8 +86,9 @@ def compute_value_at_risk_breach_statistics(
respectively. The weights should be normalized, i.e.
sum to one for each time frame.
rolling_risk_model: Optional[RollingFactorRiskModel]
The rolling risk model.
rolling_risk_model: Union[RollingFactorRiskModel, Dict[Any, Any]]
A rolling risk model object or dictionary of covariances of
which the keys and values are dates and covariances.
forecast_vols: Optional[Series]
The forecast volatility of instruments.
Expand All @@ -113,7 +115,7 @@ def compute_value_at_risk_rolling_breach_statistics(
X: ndarray,
weights: ndarray,
window: int,
rolling_risk_model: Optional[RollingFactorRiskModel] = None,
rolling_risk_model: Optional[Union[RollingFactorRiskModel, Dict[Any, Any]]] = None,
forecast_vols: Optional[Series] = None,
threshold: Optional[float] = 0.95,
min_periods: Optional[int] = None,
Expand All @@ -138,8 +140,9 @@ def compute_value_at_risk_rolling_breach_statistics(
The number of rolling time frames to compute the percentage
of returns breaching the specified VaR.
rolling_risk_model: Optional[RollingFactorRiskModel]
The rolling risk model.
rolling_risk_model: Union[RollingFactorRiskModel, Dict[Any, Any]]
A rolling risk model object or dictionary of covariances of
which the keys and values are dates and covariances.
forecast_vols: Optional[Series]
The forecast volatility of instruments.
Expand Down
18 changes: 18 additions & 0 deletions tests/accuracy/test_bias.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,21 @@ def test_compute_bias_statistics(daily_returns, weights, rolling_factor_risk_mod
index=weights.index,
)
assert_series_equal(expected_bias_statistics, bias_statistics)


def test_compute_bias_statistics_dict(
daily_returns, weights, rolling_factor_risk_model
):
covs = {date: cov for date, cov in rolling_factor_risk_model.items()}
bias_statistics = compute_bias_statistics(
X=daily_returns,
weights=weights,
rolling_risk_model=covs,
window=5,
min_periods=0,
)
expected_bias_statistics = Series(
array([nan, nan, nan, nan, nan, nan, nan, 1.8305485, 1.47255984, 1.31524515]),
index=weights.index,
)
assert_series_equal(expected_bias_statistics, bias_statistics)

0 comments on commit 3f2452d

Please sign in to comment.