Luck-Corrected Peer Performance Analysis with PeerPerformance

Overview

PeerPerformance evaluates the performance of investment funds relative to their peers, with a correction for luck that is robust to false discoveries. For each fund \(i\) it estimates three peer performance ratios that sum to one:

  • \(\hat\pi^+_i\) – the proportion of peers fund \(i\) significantly outperforms,
  • \(\hat\pi^0_i\) – the proportion of peers with equal performance,
  • \(\hat\pi^-_i\) – the proportion of peers that outperform fund \(i\).

The estimators implement the methodology of Ardia and Boudt (2018, Journal of Banking & Finance); the modified Sharpe ratio test follows Ardia and Boudt (2015, Finance Research Letters). A naive percentile rank ignores that many funds may be statistically indistinguishable; the false-discovery-rate (FDR) correction of Storey (2002) corrects for this.

library(PeerPerformance)
data("hfdata")          # 60 monthly returns for 100 anonymized hedge funds
dim(hfdata)
#> [1]  60 100

Alpha peer performance screening

alphaScreening() runs all pairwise tests and returns the three ratios for each fund. With factors = NULL it compares raw returns; supply a factors matrix to compare risk-adjusted alphas.

set.seed(1234)
rets <- hfdata[, 1:15]
sc <- alphaScreening(rets, control = list(nCore = 1))
round(rbind(pipos = sc$pipos, pizero = sc$pizero, pineg = sc$pineg), 3)[, 1:6]
#>         [,1] [,2]  [,3] [,4] [,5] [,6]
#> pipos  0.000    0 0.025    0 0.46 0.00
#> pizero 0.975    1 0.975    1 0.54 0.54
#> pineg  0.025    0 0.000    0 0.00 0.46

The three ratios sum to one for every fund. A summary() method ranks the funds and reports the win/loss counts:

summary(sc, top = 3)
#> 
#> Peer performance screening summary (alpha)
#>   Funds: 15
#> 
#>   alpha distribution: min -0.0060 | 25% -0.0002 | med 0.0022 | mean 0.0021 | 75% 0.0035 | max 0.0124
#> 
#> Top funds (by alpha):
#>     fund  n npeer estimate lambda pizero  pipos  pineg wins losses net rank
#>  Fund 12 60    14   0.0124 0.7000 0.0000 1.0000 0.0000    2      0   2    1
#>  Fund 13 60    14   0.0080 0.7000 0.0000 0.9286 0.0714    0      0   0    2
#>  Fund 15 60    14   0.0040 0.6000 0.3573 0.6427 0.0000    1      0   1    3
#> 
#> Top funds (by outperformance ratio pi+):
#>     fund  n npeer estimate lambda pizero  pipos  pineg wins losses net rank
#>  Fund 12 60    14   0.0124 0.7000 0.0000 1.0000 0.0000    2      0   2    1
#>  Fund 13 60    14   0.0080 0.7000 0.0000 0.9286 0.0714    0      0   0    2
#>  Fund 15 60    14   0.0040 0.6000 0.3573 0.6427 0.0000    1      0   1    3

The ratios are point estimates; confint() attaches confidence intervals by a nonparametric peer (pairwise) bootstrap that resamples each fund’s peers:

set.seed(1234)
round(confint(sc, parm = "pipos")[1:6, ], 3)
#>        2.5% 97.5%
#> Fund 1    0 0.140
#> Fund 2    0 0.000
#> Fund 3    0 0.386
#> Fund 4    0 0.369
#> Fund 5    0 0.821
#> Fund 6    0 0.214

The plot() method reproduces the Ardia and Boudt (2018) screening plot (funds sorted by performance; stacked out-/equal-/under-performance bars with the naive percentile-rank diagonal):

plot(alphaScreening(hfdata[, 1:30], control = list(nCore = 1)))

Sharpe and modified Sharpe ratios

round(sharpe(rets[, 1:6]), 3)
#> Fund 1 Fund 2 Fund 3 Fund 4 Fund 5 Fund 6 
#>  0.020 -0.023  0.109  0.111  0.210 -0.010
round(msharpe(rets[, 1:6], level = 0.95), 3)
#> Fund 1 Fund 2 Fund 3 Fund 4 Fund 5 Fund 6 
#>  0.011 -0.014  0.061  0.072  0.120 -0.005

Equality of two funds’ (modified) Sharpe ratios is tested with sharpeTesting() / msharpeTesting(). The asymptotic test is the default; a studentized bootstrap (recommended for short or autocorrelated series) is requested with control = list(type = 2).

res <- msharpeTesting(hfdata[, 1], hfdata[, 2], level = 0.95)
c(dmsharpe = res$dmsharpe, tstat = res$tstat, pval = res$pval)
#>   dmsharpe      tstat       pval 
#> 0.02482295 0.46138131 0.64452506

sharpeScreening() and msharpeScreening() build the peer performance ratios from Sharpe (resp. modified Sharpe) comparisons instead of alphas.

Comparing a fund (or group) against a separate peer group

The Y argument (or the convenience wrapper targetPeerPerformance()) screens a chosen fund, or subset of funds, against a separate universe.

focal <- hfdata[, 1]
peers <- hfdata[, 11:40]
res_xy <- alphaScreening(focal, Y = peers, control = list(nCore = 1))
round(c(pizero = res_xy$pizero, pipos = res_xy$pipos, pineg = res_xy$pineg), 3)
#> pizero  pipos  pineg 
#>  0.222  0.011  0.767

Control parameters

All screening/testing functions share a control list. Common fields include nCore (parallel cores), lambda (the \(\lambda\) threshold for \(\hat\pi^0\), NULL = data-driven), gammaPos/gammaNeg (the one-sided thresholds, default 0.4/0.6), hac (HAC standard errors), and for the Sharpe/mSR routines type (asymptotic/bootstrap), nBoot, and bBoot. See ?alphaScreening.

References

  • Ardia, D., Boudt, K. (2018). The peer performance ratios of hedge funds. Journal of Banking & Finance 87, 351–368.
  • Ardia, D., Boudt, K. (2015). Testing equality of modified Sharpe ratios. Finance Research Letters 13, 97–104.
  • Storey, J. (2002). A direct approach to false discovery rates. Journal of the Royal Statistical Society B 64(3), 479–498.