Code covered by the BSD License  

Highlights from
Shapley Portfolio Risk Decomposition

image thumbnail
from Shapley Portfolio Risk Decomposition by Hakan
Calculates the percentage contribution to risk of each asset/strategy/investment.

ShapleyRiskDecomposition(C,w,createplot,names)
function PCTR = ShapleyRiskDecomposition(C,w,createplot,names)
% PCTR = ShapleyRiskDecomposition(C,w,createplot,names);
%
% Calculates the Shapley percentage contibution to risk (=volatility) 
% of a set N number of assets
% Warning    : Takes too long to end if N > 20  
%
% Inputs     :
% ------------
% C          : NxN covariance matrix
% w          : Nx1 weights vector
% createplot : (=1) if a plot of risk contributions is needed 0 otherwise
% names      : Nx1 cell array of the names of assets 
%
%
% Outputs:
% ------------
% PCTR       : Nx2 matrix of percentage contribution to risk of w
%              The first column is Shapley PCTR, 
%              the second column is Euler PCTR. 
%
%
% Author: Hakan Kaya
% Date  : February 2nd, 2013

% Demos
%
% Sensitivity to correlation. What is the difference between Shapley and
% Euler decompositions if correlation (\rho) varies between two assets.
% PCTR = ShapleyRiskDecomposition; 
%
% Create random covariance and calculate risk contrib of equal weight.
% N = 10; % number of assets
% U = randn(100,N); C = U'*U/2; w = ones(N,1)/N;
% PCTR = ShapleyRiskDecomposition(C,w,1,[]);

if nargin < 1
    x = [90 45 0 -45 -90]/100;
    w = (0:1:100)/100;
    lt = cell(length(x),1);
    Y = zeros(length(w),length(x));
    X = Y;
    for i = 1:length(x)
        lt(i,1) = {['\rho = ',num2str(x(i))]};
        C = [1 x(i);x(i) 1];
        for j = 1:length(w)
            PCTR = ShapleyRiskDecomposition(C,[w(j) 1-w(j)],0,[]);
            Y(j,i) = PCTR(1,1);
            X(j,i) = PCTR(1,2);
        end
    end
    figure;
    m = min([min(min(Y)) min(min(X))]);
    M = max([max(max(Y)) max(max(X))]);
    plot(w,Y,'linewidth',3); grid on
    
    xlabel('Weight of Asset 1')
    ylabel('PCTR of Asset 1')
    hold on;
    plot(w,X,'linewidth',1); grid on
    title('Euler(Light) vs Shapley(Thick) PCT Volatility','Fontsize',15)
    
    xlabel('Weight of Asset 1')
    ylabel('PCTR of Asset 1')
    ylim([m M])
    legend(lt,'location','nw')
    drawnow;
    return;
end

if exist('createplot','var')==0
    createplot = 0;
    if exist('names','var')==0
        names = [];
    end
end

w = w(:);
n = size(C,1);
S = 1:n;
CTR = zeros(n,1);
[YY,NB] = nchoose(setdiff(S,1));
for k = 1:n
    c = 0;
    if k > 1
        YY = replaceelement(YY,k,k-1);
    end
    for s = 0:(n-1)      
        Y = YY(NB==s);
        for j = 1:size(Y,1)
            D = DeltaF(k,Y(j),w,C);
            c = c + (1 / (nchoosek(n,s)*(n-s))) * D;
        end
    end
    CTR(k,1) = c;
end
PCTR = CTR / GetF(C,w);
PCTR = [PCTR,diag(w)*C*w/ ( w'*C*w )];


if createplot == 1
   if isempty(names)
       names = 1:size(PCTR,1);
   end
   figure;
   subplot(3,1,1)
   bar(PCTR); grid on;
   xlim([0 size(PCTR,1)+1])
   title('Euler and Shapley Risk Decomposition','FontSize',15);
   legend('Shapley','Euler')
   set(gca,'xtick',1:size(PCTR,1),'xticklabel',names)
   subplot(3,1,2)
   [sd,Cr] = cov2corr(C);
   bar(sd); grid on;
   xlim([0 size(PCTR,1)+1])
   title('Volatility','FontSize',15);
   set(gca,'xtick',1:size(PCTR,1),'xticklabel',names)
   subplot(3,1,3)  
   imagesc(Cr); grid on; colorbar('Location','SouthOutside');
   set(gca,'xtick',1:size(PCTR,1),'xticklabel',names)
   set(gca,'ytick',1:size(PCTR,1),'yticklabel',names)
   title('Correlation Heatmap','FontSize',15);   
end


function YY = replaceelement(YY,k,l)
for i = 1:length(YY)
    YY{i}(YY{i}==k) = l;
end

function x = DeltaF(k,I,w,C)
J = [I{1},k];
if sum(I{1})>0
    x = GetF(C(J,J),w(J)) - GetF(C(I{1},I{1}),w(I{1}));
else
    x = GetF(C(J,J),w(J));
end


function x = GetF(C,w)
if isempty(w)==0
    x = sqrt(w'*C*w);
else
    x = 0;
end


function [W,C] = nchoose(S)
% NCHOOSE - all combinations of the elements of a set
%   W = nchoose(S) returns all possible combinations of one or more
%   elements of the set S. In total there are 2^N-1 combinations, where N
%   is the number of elements in S. W is a cell array: each cell holds one
%   of these combination (as a row vector). S can be a cell array, and each
%   cell of W will then contain a cell array.  
%
%   Example:
%      nchoose([2 4 6 8])
%        % -> { [2] ; 
%        %      [4] ; 
%        %      [2 4] ; 
%        %      [6] ;
%        %      ...
%        %      [2 6 8] ;
%        %      [4 6 8] ;
%        %      [2 4 6 8]} ; % in total (2^4)-1 = 15 different combinations
% 
%   Notes: 
%   - For sets containing more than 18 elements a warning is given, as this
%     can take some time. On my PC, a set of 20 elements took 20 seconds.
%     Hit Ctrl-C to intterupt calculations.
%   - If S contain non-unique elements (e.g. S = [1 1 2]), NCHOOSE will
%     return non-unique cells. In other words, NCHOOSE treats all elements
%     of S as being unique. One could use NCHOOSE(UNIQUE(S)) to avoid that.
%   - Loosely speaking, NCHOOSE(S) collects all output of multiple calls to
%     NCHOOSEK(S,K) where K is looping from 1 to the number of elements of
%     S. The implementation of NCHOOSE, however, does rely of a different
%     method and is much faster than such a loop.
%   - By adding an empty cell to the output, the power set of a cell array
%     is formed:
%       S = {1 ; 'hello' ; { 1 2 3}}
%       PowerS = nchoose(S) 
%       PowerS(end+1) = {[]}
%     See: http://en.wikipedia.org/wiki/Power_set
%   
%   See also NCHOOSEK, PERMS
%            COMBN, ALLCOMB on the File Exchange


N = numel(S) ; 

% How many unique combinations of 1 to N elements are there
M = (2^N)-1 ;
if N > 18,
    warning('Nchoose:LargeOutput', ...
        'There are %d unique combinations. Please be patient ...',M) ;
end

S = S(:).' ;   % make the set a row vector, for uniform output

% The selection of elements is based on the binary representation of all
% numbers between 1 and M. The binary representation of numbers stored in X
% can be calculated using the formula (see DEC2BIN):
%    B1 = rem(floor(X(:) * pow2(1-N:0)),2) 
% (* means matrix multiplication) in which row B(k,:) represents X(k)
% Based on the suggestion by US, this formula can be rewritten as:
%    B2 = bitget(X2 * (2.^(N-1:-1:0)),N) > 0
% for a specific number X2. Although the bits of B2 are in reversed order
% (compared to B1), this is, however, exactly what we want, as we can now
% select the elements of S directly. We do not need to flip the order
% afterwards. 

% Looping over the numbers is faster than creating a whole matrix at once
W = cell(M+1,1) ;    % Pre-allocation of output
p2=2.^(N-1:-1:0) ; % This part of the formula can be taken out of the loop
C = zeros(M+1,1);
for i=1:M,
    % calculate the (reversed) binary representation of i
    % select the elements of the set based on this representation
    W{i} = S(bitget(i*p2,N) > 0) ; 
    C(i,1) = length(W{i});
end
W{M+1} = [];

% Two other options, which are less efficient
% % 1) looping over nchoosek (slow)
% for i=N:-1:1,
%     W{i} = nchoosek(S,i) ;
% end
% % and now W still has to be split into distinct cells ...
% 
% % 2) in one big step (memory inefficient)
% Q = rem(floor([1:M].'*pow2(1-N:0)),2).' ;
% [R,C] = find(Q) ;
% W = mat2cell(S(R),1,sum(Q)).' ;

Contact us