Post

Value at Risk (VaR) of a single risk factor

I discuss Value at Risk of a single asset under the assumption the price changes of the latter is governed by a geometric Brownian motion.

Value at Risk (VaR) of a single risk factor

In a previous post, I introduced a commonly used quantitative measure of risk called Value at Risk (VaR) and its tight relation with the quantiles/percentiles of a distribution. There, I focused on a toy example where the change in a portfolio’s value (e.g. returns) is assumed to be normally distributed. In this post, I will compute VaR of a single asset, by mimicking a portfolio composed of a number of the same asset and modelling the price changes of the asset using the geometric Brownian motion GBM. The GBM approach will allow us to derive an analytic formula for VaR, enabling us to gain more insight to some common practices in the literature, as well as the validity of this approach with respect to methods utilized out in the real world.

Recall that infinitesimal log price changes under the GBM framework is given by the stochastic differential equation

\[\begin{equation}\label{lp} \mathrm{d}\ln(P(t)) = \mu\, \mathrm{d} t + \sigma\, \mathrm{d}W, \quad\quad \mathrm{d}W \sim \mathcal{N}(0,\mathrm{d}t) \end{equation}\]

with $\mu$ and $\sigma$ are the constant drift and volatility. The solution to this equation describes the stochastic process for the price of the asset. For a finite time span $\delta t$ later than time $t$, it is given by

\[\begin{equation}\label{solp} P(t + \delta t) = P(t)\, \mathrm{e}^{\mu\, \delta t + \sigma\, \delta W}, \quad\quad \delta W \sim \mathcal{N}(0,\delta t). \end{equation}\]

VaR of a single asset


We now consider value at risk (VaR) for a “portfolio” that consist of a single position of $N$ copies of the asset. The current value of this portfolio can be written as $V(t) = N P(t)$ and therefore the change in the portfolio value over a finite time interval $\delta t$ is given by

\[\begin{equation}\label{dVz} \delta V(t) = N (P(t + \delta t) - P(t)) = N \delta P(t). \end{equation}\]

Using the solution \eqref{solp}, it can be expressed explicitly as

\[\begin{equation}\label{dV} \delta V(t) = N P(t) \left[\mathrm{e}^{\mu\, \delta t + \sigma\, \delta W} - 1\right]. \end{equation}\]

The pre-factor $N$ is commonly referred as the sensitivity of $V$ to the underlying asset price as it amplifies the change in the former by a change in the asset price, $\delta P$. Although we consider it as the “number of assets” in this context, it is important to keep in mind that the results we will derive here are also valid for any portfolio that is linearly sensitive the underlying risk factor.

Now consider the VaR as the maximum possible decline in the portfolio value within a time horizon of $\delta t$ with a specified confidence $c$. Implicitly, the value at risk can therefore be defined as the cumulative probability where the decline in the portfolio value is above the negative of VaR, $\delta V > -\textrm{VaR(c)}$,

\[c := P(\delta V > - \textrm{VaR}).\]

Using \eqref{dV}, we can describe this probability, in terms of the GBM model parameters $\mu$, $\sigma$, the price of the asset and the liquidity period $\delta t$ as

\[c := P(\delta V > - \textrm{VaR}) = P( N P(t) \left[\mathrm{e}^{\mu\, \delta t + \sigma\, \delta W} - 1\right]> - \textrm{VaR}).\]

The key point here is to describe this probability in terms of the only random variable in this expression, namely the Brownian motion $\delta W \sim Z \sqrt{\delta t}$ where $Z$ is a standard normal variable $Z \sim \mathcal{N}(0,1)$. Using a series of algebraic manipulations in the above probability, we obtain

\[c := P(\delta V > - \textrm{VaR}) \equiv P( Z > a)\]

where

\[a \equiv \frac{1}{\sigma \sqrt{\delta t}} \ln \left(1-\frac{\mathrm{VaR}}{N P(t)}\right)-\frac{\mu}{\sigma} \sqrt{\delta t}.\]

Therefore, the $1-c$ quantile $Q_{1-c}$ of the standard normal distribution gives us the variable we defined as $a$:

\[P(Z \leq a) = 1 - c \quad \Longrightarrow \quad P^{-1}(1-c) = Q^{\mathcal{N}(0,1)}_{1-c} = \frac{1}{\sigma \sqrt{\delta t}} \ln \left(1-\frac{\mathrm{VaR}}{N P(t)}\right)-\frac{\mu}{\sigma} \sqrt{\delta t}.\]

Finally, we can invert this expression to obtain VaR in terms of the quantile of the normal distribution as

\[\begin{equation}\label{varl} \textrm{VaR}_l(c) = N P(t) \left[1 - \mathrm{e}^{\mu \delta t + Q^{\mathcal{N}(0,1)}_{1-c} \sigma \sqrt{\delta t}} \right]. \end{equation}\]

The eq. \eqref{varl} gives us the maximum loss that our investment may face with a confidence of $100\cdot c \, \%$ over time span of $\delta t$ for a long position of the asset. In other words, if we buy $N$ number of shares of this asset and hold them for a time horizon of $\delta t$, any loss due to this investment in this time period will be smaller than $\textrm{VaR}(c)$ in eq. \eqref{varl} at $100\cdot c\, \%$ confidence level.

Now, let’s consider the VaR of a short position of the asset, sending $N \to -N$ in eq. \eqref{dV}. The probability that our investment loss will be smaller than this maximal value is now given by

\[\begin{equation}\label{cs} c := P(\delta V > - \textrm{VaR}) \equiv P( Z < \tilde{a}) \end{equation}\]

with

\[\begin{equation}\label{ta} \tilde{a} \equiv \frac{1}{\sigma \sqrt{\delta t}} \ln \left(1+\frac{\mathrm{VaR}}{N P(t)}\right)-\frac{\mu}{\sigma} \sqrt{\delta t}. \end{equation}\]

For the ease of comparison with the VaR of the long position we prefer to describe VaR of the short position in terms of $Q^{\mathcal{N}(0,1)}_{1-c}$ as well. For this we re-write \eqref{cs} as

\[1 - c = P (Z \geq \tilde{a}) = \int_{\tilde a}^{\infty} f_{Z}(x)\, \mathrm{d}x\]

where $f_Z(x) = \mathrm{e}^{-x^2/2}/\sqrt{2\pi}$ is the probability density function of the standard normal distribution which is invariant under $x\to -x$. Using this symmetry property we can write the expression above (by a change of variable $x = - x’$) as

\[1 - c = - \int_{-\tilde{a}}^{-\infty} f_Z (x')\, \mathrm{d}x' = P(Z \leq -\tilde{a}).\]

This implies that $1-c$ percentile of the standard normal distribution gives $-\tilde{a}$:

\[-\tilde{a} = Q^{\mathcal{N}(0,1)}_{1-c}.\]

Inverting this expression using eq. \eqref{ta}, VaR of a short position for a single asset is given by

\[\begin{equation}\label{vars} \textrm{VaR}_s(c) = - N P(t) \left[1 - \mathrm{e}^{\mu \delta t -Q^{\mathcal{N}(0,1)}_{1-c} \sigma \sqrt{\delta t}} \right] \end{equation}\]

By comparing \eqref{varl} and \eqref{vars}, notice the asymmetry of VaR expressions. This makes sense as the underlying stock prices follow a log-normal distribution. Furthermore, the drift terms $\mu \delta t$ also influence the VaR differently. For a long position, VaR is reduced due to drift while it increases in a short position due to negative sign in front of $N$. This makes sense since the investment losses should increase as the underlying value increases for a short position.

Short liquidity periods: For short liquidity periods such as $\delta t = 10\, \textrm{days} = 0.0274\, \textrm{years}$, it is customary to linearize the exponential in the expressions for VaR in eqs. \eqref{varl} and \eqref{vars}. For such periods, it may be also viable to ignore the drift term all together with respect to volatility. Adopting the former, we have

\[\begin{align} \nonumber \textrm{VaR}_l(c)& \simeq N P(t) \left[- Q_{1-c}^{\mathcal{N}(0,1)}\sigma \sqrt{\delta t} - \mu \delta t \right] \\ \label{sl_var} \textrm{VaR}_s(c)& \simeq N P(t) \left[- Q_{1-c}^{\mathcal{N}(0,1)}\sigma \sqrt{\delta t} + \mu \delta t \right]. \end{align}\]

Once again, these expressions show the difference of the effect of the drift term $\mu \delta t > 0$, in particular $\textrm{VaR} > 0$ increases for a short position since $- Q_{1-c} > 0$. Furthermore, VaR in both cases is equivalent only when the drift is neglected. Namely, in cases when $ - Q_{1-c} \gg \mu \sqrt{\delta t} / \sigma$. On the other hand, it is only in this limit we can utilize the square root of time rule to relate the Value at Risk for a liquidation period $\delta t$ to another liquidation period $\delta t’$. Similarly, only in the same limit VaR with respect to a confidence level $c$ can be converted to VaR with respect to another confidence level $c’$:

\[\begin{equation}\label{vr} \textrm{VaR}(c',\delta t') \simeq \frac{Q_{1-c'}}{Q_{1-c}} \sqrt{\frac{\delta t'}{\delta t}}\, \textrm{VaR}(c,\delta t). \end{equation}\]

To summarize, eq. \eqref{vr} holds for short liquidity periods where we can ignore the overall the drift, and only for portfolios that are linearly sensitive to the underyling asset or when this linearity holds to a good approximation.

The usefulness of the analytical formulas in eqs. \eqref{varl},\eqref{vars} and \eqref{sl_var} is notwithstanding, they rely on a crucial assumption that the log of price changes (e.g. log returns) can be modelled by a GBM which in turn implies that the latter exhibit normal distribution. In the real world however, the distribution of log returns of asset prices usually have fat tails and skinny shoulders, somewhat resembling a t-distribution. To challenge the accuracy of the expressions we derived in this post, we will therefore utilize a method called the historical method, which along with the ‘Monte Carlo method’ and ‘Variance-covariance’ method is one of the common techniques to compute VaR of an investment. Essentially, in this method one uses historical data of an asset to directly compute VaR (as the $1-c$ th percentile of a distribution corresponding to a $100\cdot c\, \%$ percent confidence level) without making any assumption about the underlying distribution of asset returns. For this purpose, we first download 2 (trading) years of daily price data of NVDA (NVIDIA) and ENPH (a green energy company focusing on solar panels) from yfinance using finance_data class (see Appendix A) that I intend to build upon gradually in the forthcoming posts.

1
2
3
4
5
6
7
8
9
10
11
12
13
# stocks, start and end date
tickers = ['NVDA', 'ENPH']
end_date = pd.to_datetime('2023-02-10')
start_date = end_date - dt.timedelta(days = 732)

# initialize the class and download price data
price_data = finance_data(tickers, start_date, end_date)
price_data.download()

# Log returns and its summary stats
return_df = price_data.to_returns(log = True)

price_data.summary_stats()

As shown in the table, log-returns of these stocks are skewed with some extreme values far away from the mean. This is especially true of ENPH which exhibits heavy tails as implied by a kurtosis significantly larger than the normal distribution, i.e. kurtosis > 3.0 .

Log-returnsENPHNVDA
mean0.0010430.001476
std0.0433930.035081
min-0.141379-0.094726
50%0.0006950.002386
max0.2465120.143293
skew0.5033230.178566
kurtosis5.8148593.549312

We are interested in the negative values that appear on the left tail of these distributions to compute VaR. For this purpose, I have also initiated a risk class that can carry this computation through the historical method (see Appendix B). Assuming, we buy 1000 dollars $= N P(t_{\rm today})$ worth of shares (i.e. buying a fractional number of shares is allowed) today, at 95 % confidence level, the maximum loss that can incur to our investment within a time frame of $\delta t = 10 \, \rm{days}$ can be calculated via this method as

1
2
3
4
5
6
7
risk = risk(return_df) # initialize the risk class with return_df dataframe
dt = 10 # liquidity period in days 
initial_investment = 1000 # initial investment 

VaR = initial_investment * risk.historical_var() * np.sqrt(dt)

print(VaR)

Output:

ENPH    217.267653
NVDA    183.780648
dtype: float64

Notice that we utilized the square root of time rule to project all the historical information to the future as VaR essentially scale as a standard deviation. Also, this gives us another clue about one of the main assumptions about the historical method for VaR computation: namely the past behavior of an asset reflects its future performence. Setting this remark aside, the results above suggest that we do not expect to lose more than 217 (184) dollars with 95 % confidence within a time horizon of 10 days if we invest 1000 dollar to ENPH (NVDA) today. On the other hand, assuming normality of returns and neglecting the drift in the linear approximation in \eqref{sl_var} for short liquidity periods, the VaR computation gives

1
2
3
VaR_normal = initial_investment * (-norm.ppf(0.05) * return_df.std() * np.sqrt(dt))

print(VaR_normal)

Output:

ENPH    223.995951
NVDA    181.964502
dtype: float64

These results are fairly close to the value we obtained through the historical method. In particular, for both assets two methods have percent level disagreement. With these results, we may be tempted to conclude that for such short liquidity periods, one may adopt the formulas we derived in this post to obtain a fair measure of risk through VaR. However, if we were to be interested in a more tight measure of risk such as conditional value at risk (CVaR), we might also expect that the gap between the two methods to increase as CVaR (or expected shortfall) is more sensitive to the tails of the distribution. In fact, in the normal approximation to log-returns, we can formulate it for the time horizon we are interested using the square root of time rule as

\[\textrm{CVaR(c)} = (1000 $) \times [\,(1-c)^{-1} \varphi(\Phi^{-1}(1-c))\, \sigma - \mu\,] \times \sqrt{\delta t}\]

where $\varphi$ is the standard normal pdf and $\Phi^{-1}(\alpha)$ is the $\alpha$ quantile of the standard normal distribution. This gives

1
2
3
CVaR_normal = initial_investment * ((0.05)**(-1) * norm.pdf(norm.ppf(0.05)) * return_df.std() - return_df.mean()) * np.sqrt(dt)

print(CVaR_normal)

Output:

ENPH    280.539330
NVDA    225.458563
dtype: float64

On the other hand, utilizing the risk class in the Appendix B, the historical method gives

1
2
3
CVaR = initial_investment * risk.historical_cvar() * np.sqrt(10)

print(CVaR)

Output:

ENPH    296.585556
NVDA    232.439762
dtype: float64

Notice that the historical method consistently provides a tighter value for CVaR as we anticipated. The gap of disagreement now increases to roughly to $10\,\%$ level.

Conclusions

In this post, I explored VaR of a single risk factor, modelling its dynamics through geometric brownian motion. This allows us to derive an analytic formula for the VaR of portfolio consist of $N$ shares of the same asset. We put these formulas into test by comparing their performence with a commonly used VaR computation method which utilizies the past historical return data of an asset for this purpose. We found that for short liquidity periods, underlying normality assumption of the analytic formulas we derived do not introduce much error as compared to the historical method. However, if one is interested in tigther measure of risk such as CVaR (or e.g. by focusing on VaR at a higher confidence level), the disagreement gap between two methods increases. In particular, the historical method tend to output more extreme potential losses, so depending on ones risk tolerance it could be preferred for short term investments to obtain a safer measure of risk. In this post, my intention was to compare the analytic VaR computations with a practical method (historical method) of VaR that exhibit a completely different ideology. Although historical method could be also useful for short investment periods, there are other powerful methods such as Monte Carlo methods, that could be utilized for long term investments to assess risk. In a following post, I will dwell on this method, focusing on an actual portfolio composed of multiple assets.

References


1. “Derivatives and Internal Models: Modern Risk Management”, Hans-Peter Deutsch and Mark W. Beinker .

Appendix A: finance_data class


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import yfinance as yf
import numpy as np
import pandas as pd 
from scipy.stats import skew, kurtosis

class finance_data:
    
    '''Class to download and process asset price data
       TO DO: More methods... '''
    def __init__(self,stocks,start,end):
        
        self.stocks = stocks 
        self.start = start 
        self.end = end 
        
        self.price_data = None
        self.return_df = None
        
    def download(self):
        
        self.price_data = yf.download(self.stocks, self.start, self.end)
        
        if 'Close' not in self.price_data.columns:
            
            raise ValueError('Price data download failed or the ticker does not have close price data for the specified dates')
        
        else: 
            
            print(f'Price data downloaded for {self.stocks} from {self.start} to {self.end}')
            
    def to_returns(self, log = True):
        
        '''Method to compute either
           simple returns or log returns'''
        
        if self.price_data is None:
            
            raise ValueError('"Price data not downloaded. First download price data using .download() method')
            
        if log: 
            
            self.return_df =  np.log(1 + self.price_data['Close'].pct_change()).dropna() 
        
        else:
            
            self.return_df = self.price_data['Close'].pct_change().dropna()
            
        return self.return_df
        
    def summary_stats(self):
        
        '''Get descriptive statistics of returns,
           adding skewness and kurtosis to the list'''
        
        if self.return_df is None:
            
            raise ValueError('Returns are not calculated, first run .to_returns()')
            
        stats_df = self.return_df.describe() 
            
        stats_df.columns = self.price_data['Close'].columns + '_R'
         
        stats_df.loc['skew'] = skew(rets)
        # fisher flag false --> # kurtosis is not w.r.t normal dist. where kurtosis = 3.
        stats_df.loc['kurtosis'] = kurtosis(rets, fisher = False) 
                                                                        
        return stats_df

Appendix B: risk class


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class risk:
    
    def __init__(self, returns):
        
        if not isinstance(returns, (pd.Series, pd.DataFrame)):
            
            raise TypeError('Input returns must be pd.Series or pd.DataFrame object')  
        
        self.returns = returns
        
    def historical_var(self, alpha = 5):
        
        if isinstance(self.returns, pd.Series):
            
            return -np.percentile(self.returns, alpha)
        
        elif isinstance(self.returns, pd.DataFrame):
            
            return -self.returns.aggregate(lambda x: np.percentile(x, alpha))
        
    def historical_cvar(self, alpha = 5):
        
        neg_var = - self.historical_var(alpha)    
                      
        if isinstance(self.returns, pd.Series):
                                    
            below_var_returns = self.returns[self.returns <= neg_var]
            
            return -below_var_returns.mean()
                   
        elif isinstance(self.returns, pd.DataFrame):
            
            return -self.returns.apply(lambda x: x[x <= neg_var[x.name]].mean())
This post is licensed under CC BY 4.0 by the author.