Critical-Line Algortihm for Portfolio Optimization: An Open-Source Utils
最小分散ポートフォリオ
CLAでは、転換点の概念から制約付きのポートフォリオの最適化を行う。一方で、効率フロンティアの左端の最小分散ポートフォリオはこの転換点上にあるとは限らない。よって、この左端の最小分散ポートフォリオから最後の転換点までのフロンティア曲線はCLAでは求められない。与えられた$${{\bf \omega}}$$と$${{\bf V}}$$から、凸最適化によって最小分散ポートフォリオを計算するコードは以下のようになる。
def getMinVar(self):
# Get the minimum variance solution
var=[]
for w in self.w:
a=np.dot(np.dot(w.T,self.covar),w)
var.append(a)
return min(var)**.5,self.w[var.index(min(var))]
最大シャープレシオポートフォリオ
最小分散ポートフォリオの場合と同様に、一連の転換点上でシャープレシオを最大にするポートフォリオが、最大シャープレシオポートフォリオだとは限らない。二つの隣り合う転換点での解$${{\bf \omega_0}}$$と$${{\bf \omega_1}}$$の$${\alpha: \alpha\in[0,1]}$$を使った線型結合の$${{\bf \omega}= \alpha{\bf \omega}_0+ (1-\alpha){\bf \omega_1}}$$もまた解である。シャープレシオは$${\lambda}$$の単峰的関数であり、$${\omega_0}$$から$${\omega_1}$$では$${\lambda}$$は増加しない。よって、$${\lambda}$$は$${\alpha}$$の単調減少関数である。これを用いて、各転換点の間でのシャープレシオの最大値を計算し、これからフロンティア曲線全体での最大シャープレシオを求めることができる。
def goldenSection(self,obj,a,b,**kargs):
# Golden section method. Maximum if kargs['minimum']==False is passed
from math import log,ceil
tol,sign,args=1.0e-9,1,None
if 'minimum' in kargs and kargs['minimum']==False:sign=-1
if 'args' in kargs:args=kargs['args']
numIter=int(ceil(-2.078087*log(tol/abs(b-a))))
r=0.618033989
c=1.0-r
# Initialize
x1=r*a+c*b;x2=c*a+r*b
f1=sign*obj(x1,*args);f2=sign*obj(x2,*args)
# Loop
for i in range(numIter):
if f1>f2:
a=x1
x1=x2;f1=f2
x2=c*a+r*b;f2=sign*obj(x2,*args)
else:
b=x2
x2=x1;f2=f1
x1=r*a+c*b;f1=sign*obj(x1,*args)
if f1<f2:return x1,sign*f1
else:return x2,sign*f2
#---------------------------------------------------------------
def evalSR(self,a,w0,w1):
# Evaluate SR of the portfolio within the convex combination
w=a*w0+(1-a)*w1
b=np.dot(w.T,self.mean)[0,0]
c=np.dot(np.dot(w.T,self.covar),w)[0,0]**.5
return b/c
#---------------------------------------------------------------
def getMaxSR(self):
# Get the max Sharpe ratio portfolio
#1) Compute the local max SR portfolio between any two neighbor turning points
w_sr,sr=[],[]
for i in range(len(self.w)-1):
w0=np.copy(self.w[i])
w1=np.copy(self.w[i+1])
kargs={'minimum':False,'args':(w0,w1)}
a,b=self.goldenSection(self.evalSR,0,1,**kargs)
w_sr.append(a*w0+(1-a)*w1)
sr.append(b)
return max(sr),w_sr[sr.index(max(sr))]
効率フロンティア
凸最適化によって、隣り合う転換点の間の最小分散ポートフォリオ求め、効率フロンティアを導出する。
---------------------------------------------------------------
def efFrontier(self,points):
# Get the efficient frontier
mu,sigma,weights=[],[],[]
a=np.linspace(0,1,int(points/len(self.w)))[:-1] # remove the 1, to avoid duplications
b=range(len(self.w)-1)
for i in b:
w0,w1=self.w[i],self.w[i+1]
if i==b[-1]:a=np.linspace(0,1,int(points/len(self.w))) # include the 1 in the last iteration
for j in a:
w=w1*j+(1-j)*w0
weights.append(np.copy(w))
mu.append(np.dot(w.T,self.mean)[0,0])
sigma.append(np.dot(np.dot(w.T,self.covar),w)[0,0]**.5)
return mu,sigma,weights