-
Notifications
You must be signed in to change notification settings - Fork 50
Description
Summary
DownsideDeviation() returns incorrect results when MAR is a time-varying xts object. The function uses the wrong MAR values because the matrix conversion loses the time index.
Affected Functions
DownsideDeviation()SortinoRatio()(which callsDownsideDeviation())
Description
When a time-varying MAR (e.g., monthly risk-free rates) is passed to DownsideDeviation(), the function incorrectly matches MAR values to return periods. Instead of using the MAR value for each specific downside date, it uses the first N values of MAR sequentially.
Root Cause
In DownsideDeviation(), the following code path is problematic:
R = checkData(R, method = "matrix") # Converts R to plain matrix, losing time index
...
r = subset(R, R < MAR) # r is a plain matrix with no time index
if (!is.null(dim(MAR))) {
if (is.timeBased(index(MAR))) {
MAR <- MAR[index(r)] # index(r) returns 1,2,3... not dates!
}
}When R is converted to a matrix, it loses its date index. Therefore, index(r) returns integer row numbers (1, 2, 3, ...) rather than dates. The subsequent MAR[index(r)] then returns the first N values of MAR instead of the MAR values at the actual downside dates.
Minimal Reproducible Example
library(PerformanceAnalytics)
library(xts)
# Create simple return series
dates <- seq(as.Date("2020-01-01"), by = "month", length.out = 12)
returns <- xts(c(-0.05, 0.03, -0.02, 0.04, -0.01, 0.02,
-0.03, 0.05, -0.04, 0.01, -0.02, 0.03), dates)
# Time-varying MAR (e.g., different risk-free rate each month)
mar <- xts(c(0.001, 0.001, 0.001, 0.002, 0.002, 0.002,
0.003, 0.003, 0.003, 0.001, 0.001, 0.001), dates)
# PA's DownsideDeviation
pa_dd <- DownsideDeviation(returns, MAR = mar)
# Manual calculation (correct)
excess <- returns - mar
manual_dd <- sqrt(mean(pmin(excess, 0)^2))
cat("PA DownsideDeviation: ", pa_dd, "\n")
cat("Manual (correct): ", manual_dd, "\n")
cat("Values match: ", abs(pa_dd - manual_dd) < 1e-10, "\n")Expected Output
Both calculations should return the same value.
Actual Output
PA DownsideDeviation: 0.02629956
Manual (correct): 0.02715329
Values match: FALSE
Demonstrating the Index Problem
# Show what PA actually does
R <- checkData(returns, method = "matrix")
r <- subset(R, R < mar)
cat("index(r) returns row numbers, not dates:\n")
print(index(r))
# PA uses first N values of MAR
cat("\nPA uses these MAR values (first", length(r), "sequential):\n
print(as.numeric(mar[1:length(r)]))
# But should use MAR at actual downside dates
downside_mask <- as.numeric(returns) < as.numeric(mar)
correct_dates <- index(returns)[downside_mask]
cat("\nCorrect MAR values (at actual downside dates):\n")
print(as.numeric(mar[correct_dates]))Impact
SortinoRatio()with time-varying MAR returns incorrect values- The error magnitude depends on how scattered the downside periods are and how much MAR varies over time
- Fixed scalar MAR works correctly; only time-varying MAR is affected
Suggested Fix
Preserve the time index when subsetting, or extract dates before converting to matrix:
# Option 1: Store dates before matrix conversion
original_dates <- index(R)
R = checkData(R, method = "matrix")
...
r_indices <- which(R < MAR)
r = R[r_indices]
if (!is.null(dim(MAR))) {
if (is.timeBased(index(MAR))) {
MAR <- MAR[original_dates[r_indices]]
}
}Environment
- R version: 4.x
- PerformanceAnalytics version: 2.0.4 (current CRAN)
Workaround
Use a fixed scalar MAR (e.g., average risk-free rate) instead of time-varying:
# Instead of:
SortinoRatio(returns, MAR = rf_returns)
# Use:
SortinoRatio(returns, MAR = mean(rf_returns))