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 calls DownsideDeviation())
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))
Summary
DownsideDeviation()returns incorrect results whenMARis 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:When
Ris converted to a matrix, it loses its date index. Therefore,index(r)returns integer row numbers (1, 2, 3, ...) rather than dates. The subsequentMAR[index(r)]then returns the first N values of MAR instead of the MAR values at the actual downside dates.Minimal Reproducible Example
Expected Output
Both calculations should return the same value.
Actual Output
Demonstrating the Index Problem
Impact
SortinoRatio()with time-varying MAR returns incorrect valuesSuggested Fix
Preserve the time index when subsetting, or extract dates before converting to matrix:
Environment
Workaround
Use a fixed scalar MAR (e.g., average risk-free rate) instead of time-varying: