Skip to content

Commit 65e82bf

Browse files
authored
Add comprehensive Value at Risk (VaR) and Expected Shortfall (ES) calculator (#201)
1 parent a5cbeec commit 65e82bf

File tree

1 file changed

+267
-0
lines changed

1 file changed

+267
-0
lines changed
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
# Value at Risk (VaR) and Expected Shortfall (ES) Calculator
2+
# Implements multiple VaR calculation methods and Expected Shortfall
3+
# Features: Historical, Parametric, and Monte Carlo VaR/ES calculations
4+
5+
library(R6)
6+
7+
#' RiskMetrics Class
8+
#' @description R6 class for calculating Value at Risk and Expected Shortfall
9+
#' @details Implements multiple methods for VaR and ES calculation:
10+
#' - Historical simulation (non-parametric)
11+
#' - Parametric (variance-covariance)
12+
#' - Monte Carlo simulation
13+
#' Time complexity varies by method, documented per method
14+
RiskMetrics <- R6Class(
15+
"RiskMetrics",
16+
17+
public = list(
18+
#' @description Initialize risk calculator
19+
#' @param returns Historical returns data
20+
#' @param confidence_level Confidence level for VaR/ES (default 0.95)
21+
#' @param time_horizon Time horizon in days (default 1)
22+
initialize = function(returns = NULL, confidence_level = 0.95, time_horizon = 1) {
23+
private$validate_parameters(confidence_level, time_horizon)
24+
25+
self$returns <- returns
26+
self$confidence_level <- confidence_level
27+
self$time_horizon <- time_horizon
28+
29+
if (!is.null(returns)) {
30+
private$fit_distribution()
31+
}
32+
},
33+
34+
#' @description Calculate Historical VaR
35+
#' @param portfolio_value Current portfolio value
36+
#' @param method Calculation method ('historical', 'parametric', or 'monte_carlo')
37+
#' @param n_simulations Number of simulations for Monte Carlo method
38+
calculate_var = function(portfolio_value, method = "historical", n_simulations = 10000) {
39+
if (is.null(self$returns)) {
40+
stop("No returns data available. Please initialize with returns data.")
41+
}
42+
43+
method <- match.arg(method, c("historical", "parametric", "monte_carlo"))
44+
45+
var_value <- switch(method,
46+
"historical" = private$calculate_historical_var(portfolio_value),
47+
"parametric" = private$calculate_parametric_var(portfolio_value),
48+
"monte_carlo" = private$calculate_monte_carlo_var(portfolio_value, n_simulations)
49+
)
50+
51+
# Scale VaR to time horizon
52+
var_value * sqrt(self$time_horizon)
53+
},
54+
55+
#' @description Calculate Expected Shortfall (Conditional VaR)
56+
#' @param portfolio_value Current portfolio value
57+
#' @param method Calculation method ('historical', 'parametric', or 'monte_carlo')
58+
#' @param n_simulations Number of simulations for Monte Carlo method
59+
calculate_es = function(portfolio_value, method = "historical", n_simulations = 10000) {
60+
if (is.null(self$returns)) {
61+
stop("No returns data available. Please initialize with returns data.")
62+
}
63+
64+
method <- match.arg(method, c("historical", "parametric", "monte_carlo"))
65+
66+
es_value <- switch(method,
67+
"historical" = private$calculate_historical_es(portfolio_value),
68+
"parametric" = private$calculate_parametric_es(portfolio_value),
69+
"monte_carlo" = private$calculate_monte_carlo_es(portfolio_value, n_simulations)
70+
)
71+
72+
# Scale ES to time horizon
73+
es_value * sqrt(self$time_horizon)
74+
},
75+
76+
#' @description Generate risk report with multiple metrics
77+
#' @param portfolio_value Current portfolio value
78+
#' @param include_methods Which methods to include in report
79+
generate_risk_report = function(portfolio_value,
80+
include_methods = c("historical", "parametric", "monte_carlo")) {
81+
results <- list()
82+
83+
for (method in include_methods) {
84+
results[[method]] <- list(
85+
var = self$calculate_var(portfolio_value, method),
86+
es = self$calculate_es(portfolio_value, method)
87+
)
88+
}
89+
90+
# Add distribution statistics
91+
results$statistics <- list(
92+
mean_return = mean(self$returns),
93+
volatility = sd(self$returns),
94+
skewness = private$calculate_skewness(),
95+
kurtosis = private$calculate_kurtosis()
96+
)
97+
98+
return(results)
99+
},
100+
101+
#' @description Update returns data and recalculate distribution parameters
102+
#' @param new_returns New returns data to use
103+
update_returns = function(new_returns) {
104+
self$returns <- new_returns
105+
private$fit_distribution()
106+
invisible(self)
107+
},
108+
109+
# Public fields
110+
returns = NULL,
111+
confidence_level = NULL,
112+
time_horizon = NULL
113+
),
114+
115+
private = list(
116+
# Distribution parameters
117+
mean_return = NULL,
118+
volatility = NULL,
119+
120+
#' @description Fit distribution to returns data
121+
fit_distribution = function() {
122+
private$mean_return <- mean(self$returns)
123+
private$volatility <- sd(self$returns)
124+
},
125+
126+
#' @description Calculate Historical VaR
127+
calculate_historical_var = function(portfolio_value) {
128+
sorted_returns <- sort(self$returns)
129+
index <- floor((1 - self$confidence_level) * length(sorted_returns))
130+
-sorted_returns[index] * portfolio_value
131+
},
132+
133+
#' @description Calculate Parametric VaR
134+
calculate_parametric_var = function(portfolio_value) {
135+
z_score <- stats::qnorm(self$confidence_level)
136+
portfolio_value * (z_score * private$volatility - private$mean_return)
137+
},
138+
139+
#' @description Calculate Monte Carlo VaR
140+
calculate_monte_carlo_var = function(portfolio_value, n_simulations) {
141+
simulated_returns <- stats::rnorm(
142+
n_simulations,
143+
mean = private$mean_return,
144+
sd = private$volatility
145+
)
146+
sorted_returns <- sort(simulated_returns)
147+
index <- floor((1 - self$confidence_level) * n_simulations)
148+
-sorted_returns[index] * portfolio_value
149+
},
150+
151+
#' @description Calculate Historical Expected Shortfall
152+
calculate_historical_es = function(portfolio_value) {
153+
sorted_returns <- sort(self$returns)
154+
var_index <- floor((1 - self$confidence_level) * length(sorted_returns))
155+
tail_returns <- sorted_returns[1:var_index]
156+
-mean(tail_returns) * portfolio_value
157+
},
158+
159+
#' @description Calculate Parametric Expected Shortfall
160+
calculate_parametric_es = function(portfolio_value) {
161+
z_score <- stats::qnorm(self$confidence_level)
162+
phi_z <- stats::dnorm(z_score)
163+
lambda <- phi_z / (1 - self$confidence_level)
164+
portfolio_value * (lambda * private$volatility - private$mean_return)
165+
},
166+
167+
#' @description Calculate Monte Carlo Expected Shortfall
168+
calculate_monte_carlo_es = function(portfolio_value, n_simulations) {
169+
simulated_returns <- stats::rnorm(
170+
n_simulations,
171+
mean = private$mean_return,
172+
sd = private$volatility
173+
)
174+
sorted_returns <- sort(simulated_returns)
175+
var_index <- floor((1 - self$confidence_level) * n_simulations)
176+
tail_returns <- sorted_returns[1:var_index]
177+
-mean(tail_returns) * portfolio_value
178+
},
179+
180+
#' @description Calculate distribution skewness
181+
calculate_skewness = function() {
182+
r <- self$returns
183+
n <- length(r)
184+
m3 <- sum((r - mean(r))^3) / n
185+
s3 <- sd(r)^3
186+
m3 / s3
187+
},
188+
189+
#' @description Calculate distribution kurtosis
190+
calculate_kurtosis = function() {
191+
r <- self$returns
192+
n <- length(r)
193+
m4 <- sum((r - mean(r))^4) / n
194+
s4 <- sd(r)^4
195+
m4 / s4 - 3 # Excess kurtosis (normal = 0)
196+
},
197+
198+
#' @description Validate input parameters
199+
validate_parameters = function(confidence_level, time_horizon) {
200+
if (confidence_level <= 0 || confidence_level >= 1) {
201+
stop("Confidence level must be between 0 and 1")
202+
}
203+
if (time_horizon <= 0) {
204+
stop("Time horizon must be positive")
205+
}
206+
}
207+
)
208+
)
209+
210+
# Demonstration
211+
demonstrate_risk_metrics <- function() {
212+
cat("=== Value at Risk and Expected Shortfall Demo ===\n\n")
213+
214+
# Generate sample returns data
215+
set.seed(42)
216+
n_days <- 1000
217+
returns <- rnorm(n_days, mean = 0.0001, sd = 0.01)
218+
219+
# Initialize calculator
220+
risk_calc <- RiskMetrics$new(
221+
returns = returns,
222+
confidence_level = 0.95,
223+
time_horizon = 1
224+
)
225+
226+
# Portfolio parameters
227+
portfolio_value <- 1000000 # $1 million portfolio
228+
229+
cat("Portfolio Parameters:\n")
230+
cat(sprintf("Value: $%d\n", portfolio_value))
231+
cat(sprintf("Confidence Level: %.1f%%\n", risk_calc$confidence_level * 100))
232+
cat(sprintf("Time Horizon: %d day(s)\n\n", risk_calc$time_horizon))
233+
234+
# Calculate VaR using different methods
235+
methods <- c("historical", "parametric", "monte_carlo")
236+
237+
cat("Value at Risk (VaR) Results:\n")
238+
for (method in methods) {
239+
var_value <- risk_calc$calculate_var(portfolio_value, method)
240+
cat(sprintf("%s VaR: $%.2f\n", tools::toTitleCase(method), var_value))
241+
}
242+
cat("\n")
243+
244+
cat("Expected Shortfall (ES) Results:\n")
245+
for (method in methods) {
246+
es_value <- risk_calc$calculate_es(portfolio_value, method)
247+
cat(sprintf("%s ES: $%.2f\n", tools::toTitleCase(method), es_value))
248+
}
249+
cat("\n")
250+
251+
# Generate and display comprehensive risk report
252+
cat("Comprehensive Risk Report:\n")
253+
report <- risk_calc$generate_risk_report(portfolio_value)
254+
255+
cat("\nDistribution Statistics:\n")
256+
cat(sprintf("Mean Return: %.6f\n", report$statistics$mean_return))
257+
cat(sprintf("Volatility: %.6f\n", report$statistics$volatility))
258+
cat(sprintf("Skewness: %.6f\n", report$statistics$skewness))
259+
cat(sprintf("Excess Kurtosis: %.6f\n", report$statistics$kurtosis))
260+
261+
cat("\n=== Demo Complete ===\n")
262+
}
263+
264+
# Run demonstration if not in interactive mode
265+
if (!interactive()) {
266+
demonstrate_risk_metrics()
267+
}

0 commit comments

Comments
 (0)