API Docs

Manual, tutorials and complete function reference

Short-term Alpha Decay
in Treynor-Black Portfolio


In this tutorial we will compare alpha decay (i.e. alpha deterioration) of a portfolio with equal position weights and a portfolio, which position weights are computed according to the popular Treynor-Black model.

Treynor-Black model is using Single-Index Model (SIM) as an underlying assumption and. assigns weight of each position in the portfolio based on ratio of its mispricing to its nonsystematic risk. \begin{equation} w_i = \frac{\alpha_i/\sigma_i^2}{\sum_{j=1}^N \alpha_j/\sigma_j^2} \end{equation}

R Code Matlab Code
Creating Portfolio

Here we are creating a simple portfolio with only two positions. By default SPY would be used for internal Single Index Model, unless other market index is selected. We will create a "price", so that all portfolio metrics will be computed for a buy-and-hold strategy without rebalancing.

# Load PortfolioEffectHFT package
require(PortfolioEffectHFT)

# create test portfolio
timeStart="2014-10-02 09:30:00"
timeEnd="2014-10-03 16:00:00"

portfolio=portfolio_create("SPY", timeStart, timeEnd)
portfolio_settings(portfolio,portfolioMetricsMode="price",jumpsModel='all',resultsNAFilter='false')
positionAAPL=position_add(portfolio,"AAPL",100)
positionGOOG=position_add(portfolio,"GOOG",100)
positionSPY=position_add(portfolio,"SPY",100)

plot(alpha_jensens(positionAAPL),alpha_jensens(positionGOOG), title="Jensen's Alpha",legend=c("AAPL","GOOG"))
Jensen's Alphas of Positions
% create test portfolio
timeStart =  '2014-10-02 09:30:00';
timeEnd = '2014-10-03 16:00:00';

portfolio=portfolio_create('index','SPY', 'fromTime',timeStart, 'toTime',timeEnd);
portfolio_settings(portfolio,'portfolioMetricsMode','price','jumpsModel','all');
portfolio_addPosition(portfolio,'AAPL',100);
portfolio_addPosition(portfolio,'GOOG',100);
portfolio_addPosition(portfolio,'SPY',100);

figure('position',[800 200 1000 700])
util_plot2d(position_jensensAlpha(portfolio,'AAPL'),'AAPL','Title','Jensens Alpha')+util_line2d(position_jensensAlpha(portfolio,'GOOG'), 'GOOG')
Jensen's Alphas of Positions

Computing Optimal Weights

We now compute optimal weights according to the Treynor-Black model. Matrix optimWeigth contains contains optimal weights for AAPL and GOOG correspondingly.

# compute optimal weights according to the Treynor-Black model
timeUTC=compute(alpha_jensens(positionAAPL))[[1]][,1]
alpha=cbind (compute(alpha_jensens(positionAAPL))[[1]][,2], compute(alpha_jensens(positionGOOG))[[1]][,2])
variance=cbind(compute(variance(positionAAPL))[[1]][,2], compute(variance(positionGOOG))[[1]][,2])

treynorBlack=alpha/variance
optimWeigth=treynorBlack/rowSums(abs(treynorBlack))

# plot optimal position weights
plot(create_metric(cbind(timeUTC[!(is.na(optimWeigth[,1]))],optimWeigth[!(is.na(optimWeigth[,1])),1]),"AAPL"),
     create_metric(cbind(timeUTC[!(is.na(optimWeigth[,2]))], optimWeigth[!(is.na(optimWeigth[,2])),2]),"GOOG"), title="Optimal Weight")
Optimal Position Weights
% compute optimal weights according to the Treynor-Black model
paren = @(x, varargin) x(varargin{:});
alpha = [paren(position_jensensAlpha(portfolio,'AAPL'),:,2),paren(position_jensensAlpha(portfolio,'GOOG'),:,2)];
timeUTC =paren(position_jensensAlpha(portfolio,'AAPL'),:,1);
variance= bsxfun(@minus,[paren(position_variance(portfolio,'AAPL'),:,2),paren(position_variance(portfolio,'GOOG'),:,2)],(paren(position_beta(portfolio,'GOOG'),:,2).^2).*paren(position_variance(portfolio,'SPY'),:,2));

treynorBlack=alpha./variance;
optimWeigth=bsxfun(@rdivide,treynorBlack,sum(abs(treynorBlack),2));

% plot optimal position weights
util_plot2d([timeUTC, optimWeigth(:,1)],'AAPL','Title','Optimal Weight')+util_line2d([timeUTC, optimWeigth(:,2)], 'GOOG')
Optimal Position Weights

Optimal Portfolio Alpha

To see, how optimal weighting scheme imporves our portfolio alphas, we contruct another portfolio where positions have equal weights.

Chart below compares mean alpha levels in the two portfolios, and, as you can see, optimal portfolio has ~40% higher mean alpha level then simple portfolio

# compute optimal position quantities for a portfolio of given size
portfolioCash=10000000
optimPosition=portfolioCash*optimWeigth/cbind(compute(price(positionAAPL))[[1]][,2],compute(price(positionGOOG))[[1]][,2])

portfolioSimple=portfolio_create("SPY", timeStart, timeEnd)
portfolio_settings(portfolioSimple,portfolioMetricsMode="price",jumpsModel='all')
positionAAPLSimple=position_add(portfolioSimple, "AAPL", quantity = (portfolioCash/2)%/%(compute(price(positionAAPL))[[1]][,2]), time = timeUTC)
positionGOOGSimple=position_add(portfolioSimple, "GOOG", quantity = (portfolioCash/2)%/%(compute(price(positionGOOG))[[1]][,2]), time = timeUTC)

portfolioOptimal=portfolio_create("SPY", timeStart, timeEnd)
portfolio_settings(portfolioOptimal,portfolioMetricsMode="price",jumpsModel='all')
positionAAPLOptimal=position_add(portfolioOptimal,"AAPL", quantity = optimPosition[,1], time = timeUTC)
positionGOOGOptimal=position_add(portfolioOptimal,"GOOG", quantity = optimPosition[,2], time = timeUTC)

meanAAPL=mean(compute(price(positionAAPLOptimal))[[1]][,2])
meanGOOG=mean(compute(price(positionGOOGOptimal))[[1]][,2])

portfolioSimpleAlpha=compute(alpha_jensens(portfolioSimple))[[1]]
portfolioOptimAlpha=compute(alpha_jensens(portfolioOptimal))[[1]]

plot(alpha_jensens(portfolioSimple),alpha_jensens(portfolioOptimal), title="Jensen's Alpha",legend=c("Simple portfolio","Optimal portfolio"))+
util_line2d(cbind(timeUTC, mean(portfolioSimpleAlpha[,2])), legend="Avg. of simple")+
util_line2d(cbind(timeUTC, mean(portfolioOptimAlpha[,2])), legend="Avg. of optimal")
Alphas of The Two Portfolios
meanGOOG=mean(paren(position_price(portfolio,'GOOG'),:,2))
meanAAPL=mean(paren(position_price(portfolio,'AAPL'),:,2))

% compute optimal position quatities for a portfolio of given size
portfolioCash=10000000;
optimPosition=portfolioCash*optimWeigth./[paren(position_price(portfolio,'AAPL'),:,2),paren(position_price(portfolio,'GOOG'),:,2)];

portfolioSimple=portfolio_create('index','SPY', 'fromTime',timeStart, 'toTime',timeEnd);
portfolio_settings(portfolioSimple,'portfolioMetricsMode','price','jumpsModel','all');
portfolio_addPosition(portfolioSimple, 'AAPL', bsxfun(@rdivide,(portfolioCash/2),(paren(position_price(portfolio,'AAPL'),:,2))), 'time', timeUTC)
portfolio_addPosition(portfolioSimple, 'GOOG',bsxfun(@rdivide,(portfolioCash/2),(paren(position_price(portfolio,'GOOG'),:,2))),  'time', timeUTC)

portfolioOptimal=portfolio_create('index','SPY', 'fromTime',timeStart, 'toTime',timeEnd);
portfolio_settings(portfolioOptimal,'portfolioMetricsMode','price','jumpsModel','all');
portfolio_addPosition(portfolioOptimal,'AAPL', optimPosition(:,1), 'time',timeUTC);
portfolio_addPosition(portfolioOptimal,'GOOG', optimPosition(:,2), 'time',timeUTC);

portfolioSimpleAlpha = portfolio_jensensAlpha(portfolioSimple);
portfolioOptimAlpha = portfolio_jensensAlpha(portfolioOptimal);

util_plot2d(portfolioSimpleAlpha,'Simple portfolio','Title', 'Jensens Alpha')+util_line2d(portfolioOptimAlpha, 'Optimal portfolio')+...
util_line2d([timeUTC, mean(portfolioSimpleAlpha(:,2))*ones(length(timeUTC),1)],'Avg. of simple')+...
util_line2d([timeUTC, mean(portfolioOptimAlpha(:,2))*ones(length(timeUTC),1)], 'Avg. of optimal')
Alphas of The Two Portfolios

Alpha Decay Model

To study properties of Jensen's alpha in the original and optimal portfolios, we fit an ARIMA(1,1,0) model to the alphas series and computed 1 step ahead forecast errors for both portfolios.

As you may see, optimal portfolio weights produced by the Treynor-Balck model display a lower forecast error compared to the equal weights scheme. In other words, alpha decay becomes more predictable for the optimal portfolio.

require(forecast)
meanSimple=NULL
forecastErrorsSimple=NULL

#forecast test for simple portfolio
#use ARIMA(1,1,0). 1 second foreacast.
for(x1 in seq(0,2,0.1)){
  x2=1-x1
  set_quantity(positionAAPLSimple, (portfolioCash*x1)%/%meanAAPL)
  set_quantity(asset=positionGOOGSimple, quantity=(portfolioCash*x2)%/%meanGOOG)
  
  alpha=compute(alpha_jensens(portfolioSimple))[[1]]
  meanSimple=c(meanSimple,mean(alpha[,2]))
  
  forecastErrors=array(0,dim=100)
  for(i in 1:100){
    if((i/21+x1*1000/21)%%5==0){
      print(paste(i/21+x1*1000/21,"%"))
    }
    fit=arima(alpha[(3200+i*400):(3400+i*400-1),2],c(1,1,0)) #ARIMA estimation 
    forecastErrors[i]=abs(forecast(fit,1)$mean-alpha[3400+i*400,2])/mean(abs(alpha[(3200+i*400):(3400+i*400-1),2])) #Calculation of forecast errors 
  } 
  forecastErrorsSimple=c(forecastErrorsSimple,mean(forecastErrors))
}

#forecast test for optimal portfolio
for(i in 1:100){
  if(i%%5==0){
    print(paste(i,"%"))
  }
  fit=arima(portfolioOptimAlpha[(3200+i*400):(3400+i*400-1),2],c(1,1,0)) #ARIMA estimation 
  forecastErrors[i]=abs(forecast(fit,1)$mean-portfolioOptimAlpha[3400+i*400,2])/abs(mean(portfolioOptimAlpha[(3200+i*400):(3400+i*400-1),2])) #Calculation of forecast errors 
} 
forecastErrorsOptim=mean(forecastErrors) #Calculation of average values 

resultdf=data.frame(err=c(forecastErrorsSimple,forecastErrorsOptim) ,mean=c(meanSimple,mean(portfolioOptimAlpha[,2])),legend=c(array("Portfolio Simple Alpha",dim=21),"Portfolio Optimal Alpha"))

ggplot() + geom_point(data=resultdf, aes(x=err, y=mean,colour=legend),size=5) +
  xlab("Forecast Error(%)") + 
  ylab("Alpha mean") +
  util_plotTheme(base_size = 15)+util_colorScheme()
Forecast Errors
meanSimple=zeros(21, 1);
forecastErrorsSimple=zeros(21, 1);
model=arima(1,1,0);
x=0:0.1:2;

for  j = 1:21
  x1=x(j); 
  x2=1-x1;

  position_setQuantity(portfolioSimple, 'AAPL', (portfolioCash*x1)/meanAAPL)
  position_setQuantity(portfolioSimple, 'GOOG', (portfolioCash*x2)/meanGOOG)
  
  alpha=portfolio_jensensAlpha(portfolioSimple);
  meanSimple(j,1)=mean(alpha(:,2));
  
  forecastErrors=zeros(100,1);
    for i =1:100
    fit=estimate(model,alpha((3200+i*400):(3400+i*400-1),2),'Display','off');
    forecastErrors(i,1)=abs( forecast(fit,1,'Y0',alpha((3200+i*400):(3400+i*400-1),2)) - alpha(3400+i*400,2))*100/mean(abs(alpha(3400+i*400,2)));       %Calculation of forecast errors
    end
    forecastErrorsSimple(j,1)=mean(forecastErrors);
end

x = -2:0.25:2;
[X,Y] = meshgrid(x);
Z = X.*exp(-X.^2-Y.^2);
contour3(X,Y,Z,30)

 for i =1:100
    fit=estimate(model,portfolioOptimAlpha((3200+i*400):(3400+i*400-1),2),'Display','off');
    forecastErrors(i,1)=abs( forecast(fit,1,'Y0',portfolioOptimAlpha((3200+i*400):(3400+i*400-1),2)) - portfolioOptimAlpha(3400+i*400,2))*100/mean(abs(portfolioOptimAlpha(3400+i*400,2)));       %Calculation of forecast errors
    end
    forecastErrorsOptim=mean(forecastErrors);

plot(forecastErrorsSimple,meanSimple,'o');
set(gca,'Color',[213/255 228/255 235/255]);
set(gcf,'Color',[213/255 228/255 235/255]);
xlim([-0.1 max(forecastErrorsSimple)]);
xlabel('Forecast Error(%)')
ylabel('Alpha mean')
legend('Portfolio Simple Alpha');
hold all
plot(forecastErrorsOptim,mean(portfolioOptimAlpha(:,2)),'o');
legend('Portfolio Optimal Alpha');
leg=get(legend(gca),'String');
legend([leg 'Portfolio Optimal Alpha']);
hold off
Forecast Errors