Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New feature: Value at Risk (VaR) #106

Merged
merged 16 commits into from
Jul 23, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,8 @@ look at the examples provided in `./example`.
`./example/Example-Analysis.py`: This example shows how to use an instance of `finquant.portfolio.Portfolio`, get the portfolio's quantities, such as
- Expected Returns,
- Volatility,
- Sharpe Ratio.
- Sharpe Ratio,
- Value at Risk.

It also shows how to extract individual stocks from the given portfolio. Moreover it shows how to compute and visualise:
- the different Returns provided by the module `finquant.returns`,
Expand Down
12 changes: 9 additions & 3 deletions example/Example-Analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@

# <markdowncell>

# ## Expected Return, Volatility and Sharpe Ratio of Portfolio
# The annualised expected return and volatility as well as the Sharpe Ratio are automatically computed. They are obtained as shown below.
# The expected return and volatility are based on 252 trading days by default. The Sharpe Ratio is computed with a risk free rate of 0.005 by default.
# ## Expected Return, Volatility, Sharpe Ratio and Value at Risk of Portfolio
# The annualised expected return and volatility, as well as the Sharpe Ratio and Value at Risk are automatically computed. They are obtained as shown below.
# The expected return and volatility are based on 252 trading days by default.
# The Sharpe Ratio is computed with a risk free rate of 0.005 by default. The Value at Risk is computed with a confidence level of 0.95 by default.

# <codecell>

Expand All @@ -68,6 +69,11 @@
# Sharpe ratio (computed with a risk free rate of 0.005 by default)
print(pf.sharpe)

# <codecell>

# Value at Risk (computed with a confidence level of 0.95 by default)
print(pf.var)

# <markdowncell>

# ## Getting Skewness and Kurtosis of the stocks
Expand Down
40 changes: 39 additions & 1 deletion finquant/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- Expected (annualised) Return,
- Volatility,
- Sharpe Ratio,
- Value at Risk,
- Beta parameter (optional),
- skewness of the portfolio's stocks,
- Kurtosis of the portfolio's stocks,
Expand Down Expand Up @@ -58,7 +59,7 @@
from finquant.efficient_frontier import EfficientFrontier
from finquant.market import Market
from finquant.monte_carlo import MonteCarloOpt
from finquant.quants import sharpe_ratio, weighted_mean, weighted_std
from finquant.quants import sharpe_ratio, value_at_risk, weighted_mean, weighted_std
from finquant.returns import (
cumulative_returns,
daily_log_returns,
Expand All @@ -85,9 +86,11 @@ def __init__(self):
self.expected_return = None
self.volatility = None
self.sharpe = None
self.var = None
self.skew = None
self.kurtosis = None
self.totalinvestment = None
self.var_confidence_level = 0.95
fmilthaler marked this conversation as resolved.
Show resolved Hide resolved
self.risk_free_rate = 0.005
self.freq = 252
# instance variables for Efficient Frontier and
Expand Down Expand Up @@ -154,6 +157,20 @@ def market_index(self, index: Market) -> None:
"""
self.__market_index = index

@property
def var_confidence_level(self):
return self.__var_confidence_level

@var_confidence_level.setter
def var_confidence_level(self, val):
if not isinstance(val, float):
raise ValueError("confidence level is expected to be a float.")
if val >= 1 or val <= 0:
raise ValueError("confidence level is expected to be between 0 and 1.")
self.__var_confidence_level = val
# now that this changed, update VaR
self._update()

def add_stock(self, stock: Stock) -> None:
"""Adds a stock of type ``Stock`` to the portfolio. Each time ``add_stock``
is called, the following instance variables are updated:
Expand Down Expand Up @@ -210,6 +227,7 @@ def _update(self):
self.expected_return = self.comp_expected_return(freq=self.freq)
self.volatility = self.comp_volatility(freq=self.freq)
self.sharpe = self.comp_sharpe()
self.var = self.comp_var()
self.skew = self._comp_skew()
self.kurtosis = self._comp_kurtosis()
if self.market_index is not None:
Expand Down Expand Up @@ -355,6 +373,22 @@ def comp_sharpe(self):
self.sharpe = sharpe
return sharpe

def comp_var(self):
"""Compute and return the Value at Risk of the portfolio.

:Output:
:VaR: ``float``, the Value at Risk of the portfolio
"""
# compute the Value at Risk of the portfolio
var = value_at_risk(
investment=self.totalinvestment,
mu=self.expected_return,
sigma=self.volatility,
conf_level=self.var_confidence_level,
)
self.var = var
return var

def comp_beta(self) -> float:
"""Compute and return the Beta parameter of the portfolio.

Expand Down Expand Up @@ -631,6 +665,7 @@ def properties(self):
- Expected Return,
- Volatility,
- Sharpe Ratio,
- Value at Risk,
- Beta (optional),
- skewness,
- Kurtosis
Expand All @@ -648,6 +683,9 @@ def properties(self):
string += f"\nPortfolio Expected Return: {self.expected_return:0.3f}"
string += f"\nPortfolio Volatility: {self.volatility:0.3f}"
string += f"\nPortfolio Sharpe Ratio: {self.sharpe:0.3f}"
string += f"\nPortfolio Value at Risk: {self.var:0.3f}"
string += f"\nConfidence level of Value at Risk: "
string += f"{str(round(self.var_confidence_level * 100))} %"
fmilthaler marked this conversation as resolved.
Show resolved Hide resolved
if self.beta is not None:
string += f"\nPortfolio Beta: {self.beta:0.3f}"
string += "\n\nSkewness:"
Expand Down
29 changes: 29 additions & 0 deletions finquant/quants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import numpy as np
import pandas as pd
from scipy.stats import norm


def weighted_mean(means, weights):
Expand Down Expand Up @@ -74,6 +75,34 @@ def sharpe_ratio(exp_return, volatility, risk_free_rate=0.005):
return (exp_return - risk_free_rate) / float(volatility)


def value_at_risk(investment, mu, sigma, conf_level=0.95) -> float:
"""Computes and returns the expected value at risk of an investment/assets.

:Input:
:investment: ``float``/``int``, total value of the investment
:mu: ``float``/``int`` average/mean return of the investment
:sigma: ``float``/``int`` standard deviation of the investment
:conf_level: ``float`` (default= ``0.95``), confidence level of the VaR

:Output:
:Value at Risk: ``float``, VaR of the investment
"""
if not isinstance(
investment, (int, float, np.int32, np.int64, np.float32, np.float64)
):
raise ValueError("investment is expected to be an integer or float.")
if not isinstance(mu, (int, float, np.int32, np.int64, np.float32, np.float64)):
raise ValueError("mu is expected to be an integer or float")
if not isinstance(sigma, (int, float, np.int32, np.int64, np.float32, np.float64)):
raise ValueError("sigma is expected to be an integer or float")
if not isinstance(conf_level, float):
raise ValueError("confidence level is expected to be a float.")
if conf_level >= 1 or conf_level <= 0:
fmilthaler marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError("confidence level is expected to be between 0 and 1.")

return investment * (mu - sigma * norm.ppf(1 - conf_level))


def annualised_portfolio_quantities(
weights, means, cov_matrix, risk_free_rate=0.005, freq=252
):
Expand Down
9 changes: 4 additions & 5 deletions finquant/stock.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ class Stock(Asset):

"""


def __init__(self, investmentinfo: pd.DataFrame, data: pd.Series) -> None:
"""
:Input:
Expand Down Expand Up @@ -78,19 +77,19 @@ def comp_beta(self, market_daily_returns: pd.Series) -> float:

def properties(self):
"""Nicely prints out the properties of the stock: Expected Return,
Volatility, Skewness, Kurtosis as well as the ``Allocation`` (and other
Volatility, Beta (optional), Skewness, Kurtosis as well as the ``Allocation`` (and other
information provided in investmentinfo.)
"""
# nicely printing out information and quantities of the stock
string = "-" * 50
string += f"\n{self.asset_type}: {self.name}"
string += f"\nExpected Return: {self.expected_return:0.3f}"
string += f"\nVolatility: {self.volatility:0.3f}"
string += f"\nSkewness: {self.skew:0.5f}"
string += f"\nKurtosis: {self.kurtosis:0.5f}"
if self.beta is not None:
string += f"\n{self.asset_type} Beta: {self.beta:0.3f}"
string += f"\nSkewness: {self.skew:0.5f}"
string += f"\nKurtosis: {self.kurtosis:0.5f}"
string += "\nInformation:"
string += "\n" + str(self.investmentinfo.to_frame().transpose())
string += "\n" + "-" * 50
print(string)
print(string)
34 changes: 34 additions & 0 deletions tests/test_quants.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import numpy as np
import pytest

from finquant.quants import (
annualised_portfolio_quantities,
sharpe_ratio,
value_at_risk,
weighted_mean,
weighted_std,
)
Expand Down Expand Up @@ -32,6 +34,38 @@ def test_sharpe_ratio():
assert sharpe_ratio(0.5, 0.22, 0.005) == 2.25


def test_value_at_risk():
assert abs(value_at_risk(1e2, 0.5, 0.25, 0.95) - 91.12) <= 1e-1
assert abs(value_at_risk(1e3, 0.8, 0.5, 0.99) - 1963.17) <= 1e-1
assert abs(value_at_risk(1e4, -0.1, 0.25, 0.9) - 2203.88) <= 1e-1
assert abs(value_at_risk(1e4, 0.1, -0.25, 0.9) - (-2203.88)) <= 1e-1
assert abs(value_at_risk(1e4, -0.1, -0.25, 0.9) - (-4203.88)) <= 1e-1
assert value_at_risk(0, 0.1, 0.5, 0.9) == 0
assert abs(value_at_risk(1e4, 0, 0.5, 0.9) - 6407.76) <= 1e-1
assert abs(value_at_risk(1e4, 0.1, 0, 0.9) - 1000) <= 1e-1
assert value_at_risk(1e4, 0, 0, 0.9) == 0


def test_value_at_risk_invalid_types():
with pytest.raises(ValueError):
value_at_risk("10000", 0.05, 0.02, 0.95)

with pytest.raises(ValueError):
value_at_risk(10000, 0.05, "0.02", 0.95)

with pytest.raises(ValueError):
value_at_risk(10000, [0.05], 0.02, 0.95)

with pytest.raises(ValueError):
value_at_risk(10000, 0.05, 0.02, "0.95")

with pytest.raises(ValueError):
value_at_risk(10000, 0.05, 0.02, 1.5)

with pytest.raises(ValueError):
value_at_risk(10000, 0.05, 0.02, -0.5)

fmilthaler marked this conversation as resolved.
Show resolved Hide resolved

def test_annualised_portfolio_quantities():
x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
y = np.array([9, 8, 7, 6, 5, 4, 3, 2, 1])
Expand Down
4 changes: 2 additions & 2 deletions version
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version=0.3.2
release=0.3.2
version=0.4.0
release=0.4.0