Skip to main content

A package for asset allocation

Project description

Efficient-Frontier

We implement an Efficient Frontier that plots the optimal expected portfolio return against a certain level of risk measured by portfolio variance. This is achieved by constraint optimisation using predefined portfolio constraints and iterating across expected levels of return from the Lowest-Variance portfolio to the maximum possible return of the portfolio with the maximum Sharpe Ratio. We define various constraints to calculate the weightings for the minVar portfolio(Minimum Variance), the MaxSR(Maximum Sharpe Ratio Portfolio) and the optimisation required to calculate variances after the optimal return weightings for all the target returns that lie between. This requires us to define portfolio performance functions which will be the focus of our code in the beginning.

First we need to import all the following libraries.

python -m pip install yfinance
python -m pip install plotly
python -m pip install scipy
python -m pip install pandas
python -m pip install datetime
pip install -i https://test.pypi.org/simple/ portfiel

Firstly we'd need to set up the covariance matrix and the expected returns of the multiple stocks to be added to multi-stock porttfolio. We extract data using the pandas datareader(Yahoo) and set up the requisites in the following section.

#Importing Data from YFin
def getData(stocks, start, end):
  stockData = pdr.get_data_yahoo(stocks, start = start, end = end)
  stockData = stockData['Close']

  returns = stockData.pct_change()
  meanReturns = returns.mean()
  covMatrix = returns.cov()
  return meanReturns, covMatrix

image

image

For a Generalised Multi-Stock Portfolio LinAlg implementation using a 3-stock portfolio as an example, we have the following theoretical computations:

image

#Defining Portfolio Performance 
def portfolioPerformance(weights, meanReturns, covMatrix):
  returns = np.sum(meanReturns * weights) * 252   #No. of Trading Day
  std = np.sqrt(np.dot(weights.T, np.dot(covMatrix, weights))) * np.sqrt(252)    #Volatility is Sigma*Sqrt(T)
  #Converting equation above to matrix form. Variance is add-able and we use 252 as it is the number of trading days.
  return returns, std

Now we use SLSQP Minimisation for most of our purposes as it is easy to use and is offered by Scipy. The first of the optimisations involve, building the portfolio with Maximum Sharpe Ratio, however as we have a minimisation algorithm, we need to define a negativeSR function that would be minimised in order to find the weightings for the MaxSR portfolio based on the following constraints.

image

The sc.minimize function first takes the function to be optimised as the first argument, the optimisation would take place over the first argument of the function to be optimised. Essentially, if negativeSR is the argument to sc.minimize, the minimisation of negativeSR would take place by varying the first argument of negativeSR, i.e, weights. The second argument for sc.minimise is the initialisation for the weights parameter. In this case we make a vector as long as the number of assets with equal weights(1/Number of Assets). Bounds are defined as the set of values that can be taken by the weights parameter for each stock, and constraints define the ones delineated in the image above.

def negativeSR(weights, meanReturns, covMatrix, riskFreeRate = 0):
  pReturns, pStd = portfolioPerformance(weights, meanReturns, covMatrix)
  return -(pReturns - riskFreeRate)/pStd  
  #Returning NSharpe Ratio as -(Excess Return over Market/Risk Taken)

def maxSR(meanReturns, covMatrix, riskFreeRate = 0, constraintSet = (0,1)):
  # Minimise the NegativeSR by altering the weights of the portfolio.
  numAssets = len(meanReturns)
  args = (meanReturns, covMatrix, riskFreeRate)
  constraints = ({'type':'eq', 'fun': lambda x : np.sum(x) - 1})
  bound = constraintSet
  bounds = list(bound for asset in range(numAssets))
  result = sc.minimize(negativeSR, numAssets * [1./ numAssets], args = args, method = 'SLSQP', bounds = bounds, constraints = constraints)
  return result

Similarly we define a minVar function:

def minimizeVariance(meanReturns, covMatrix, riskFreeRate = 0, constraintSet = (0,1)):
  #Minimise the portfolio variance by altering the weights/allocation of assets in the portfolio
  numAssets = len(meanReturns)
  args = (meanReturns, covMatrix)
  constraints = ({'type':'eq', 'fun': lambda x : np.sum(x) - 1})
  bound = constraintSet
  bounds = list(bound for asset in range(numAssets))
  result = sc.minimize(portfolioVariance, numAssets * [1./ numAssets], args = args, method = 'SLSQP', bounds = bounds, constraints = constraints)

  return result

Now that we have the range of target returns, all we need to do is iterate and minimize portfolio variance for all target return values that lie between the lowest variance and optimal variance. We have to prepare a generalised optimisation function that though similar to previous optimisers, additionally takes as a constraint, the target return.

def efficientOpt(meanReturns, covMatrix, returnTarget, constraintSet = (0,1)):
  'For each returnTarget, we want to optimise the portfolio for minimum variance.'
  numAssets = len(meanReturns)
  args = (meanReturns, covMatrix)
  constraints = ({'type':'eq', 'fun':lambda x: portfolioReturn(x, meanReturns, covMatrix)- returnTarget},{'type':'eq', 'fun': lambda x : np.sum(x) - 1})  #Can be an inequality(>=)
  bound = constraintSet
  bounds = tuple(bound for asset in range(numAssets))
  effOpt = sc.minimize(portfolioVariance, numAssets * [1./numAssets], args = args, constraints = constraints, method = 'SLSQP', bounds = bounds)

  return effOpt

Now finally we have a main function to call all of these previously defined functions and returns the Expected Return, Variance and the portfolio weights for each iteration. We will use this to plot the frontier in the subsequent step.

def calculatedResults(meanReturns, covMatrix, riskFreeRate = 0, constraintSet = (0,1)):
  '''Function to read Mean, CovMatrix and other Financial Information
  Output Maximum Sharpe Ratio, Min Volatility and Efficient Frontier'''

  #For Maximum Sharpe Ratio
  maxSR_Portfolio = maxSR(meanReturns,covMatrix)
  maxSR_Returns, maxSR_std = portfolioPerformance(maxSR_Portfolio['x'], meanReturns, covMatrix)
  maxSR_allocation = pd.DataFrame(maxSR_Portfolio['x'], index = meanReturns.index, columns = ['Allocation'])
  maxSR_allocation.Allocation = [round(i*100,0) for i in maxSR_allocation.Allocation]

  #For Min Volatitlity Portfolio
  minVol_Portfolio = minimizeVariance(meanReturns,covMatrix)
  minVol_Returns, minVol_std = portfolioPerformance(minVol_Portfolio['x'], meanReturns, covMatrix)
  minVol_allocation = pd.DataFrame(minVol_Portfolio['x'], index = meanReturns.index, columns = ['Allocation'])
  minVol_allocation.Allocation = [round(i*100,0) for i in minVol_allocation.Allocation]

  
  #Efficient Frontier
  efficientList = []
  targetReturns = np.linspace(minVol_Returns, maxSR_Returns, 100)
  for target in targetReturns:
    efficientList.append(efficientOpt(meanReturns, covMatrix, target)['fun'])

  maxSR_Returns, maxSR_std = round(maxSR_Returns * 100,2), round(maxSR_std*100,2)
  minVol_Returns, minVol_std = round(minVol_Returns * 100,2), round(minVol_std*100,2)

  return maxSR_Returns, maxSR_std, maxSR_allocation, minVol_Returns, minVol_std, minVol_allocation, efficientList, targetReturns

Finally we use plotly to plot the efficient frontier.

def EF_graph(meanReturns, covMatrix, riskFreeRate = 0, constraintSet = (0,1)):
  'Return a graph plotting the Min. Volatility, MaxSR and EfficientFrontier'
  maxSR_Returns, maxSR_std, maxSR_allocation, minVol_Returns, minVol_std, minVol_allocation, efficientList, targetReturns = calculatedResults(meanReturns, covMatrix)
  
  #Max SR
  MaxSharpeRatio = go.Scatter(
      name = 'Maximum Sharpe Ratio',
      mode = 'markers',
      x = [maxSR_std],
      y = [maxSR_Returns],
      marker = dict(color = 'red', size = 14, line = dict(width = 3, color = 'black'))
  )

  #Min Vol.   
  MinVol = go.Scatter(
      name = 'Minimum Volatility',
      mode = 'markers',
      x = [minVol_std],
      y = [minVol_Returns],
      marker = dict(color = 'green', size = 14, line = dict(width = 3, color = 'black'))   
  )

  #Efficient Frontier   
  EF_curve = go.Scatter(
      name = 'Efficent Frontier',
      mode = 'lines',
      x = [round(ef_std*100, 2) for ef_std in efficientList],
      y = [round(target*100, 2) for target in targetReturns],
      line = dict(color = 'blue', width = 4, dash = 'dashdot')   
  )

  data = [MaxSharpeRatio, MinVol, EF_curve]

  layout = go.Layout(
      title = 'Portfolio Optimization with Efficient Frontier',
      yaxis = dict(title = 'Annualised Return(%)'),
      xaxis = dict(title = 'Annualised Volatility(%)'),
      width = 800,
      height = 600,      
      showlegend = True,
      legend = dict(
          x = 0.75, y = 0, traceorder = 'normal',
          bgcolor = '#E2E2E2',
          bordercolor = 'black',
          borderwidth = 2),
          )

  fig = go.Figure(data = data,layout = layout)
  return fig.show()
  
EF_graph(meanReturns, covMatrix)

image

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

portfiel-0.1.4.tar.gz (8.4 kB view details)

Uploaded Source

Built Distribution

portfiel-0.1.4-py3-none-any.whl (7.8 kB view details)

Uploaded Python 3

File details

Details for the file portfiel-0.1.4.tar.gz.

File metadata

  • Download URL: portfiel-0.1.4.tar.gz
  • Upload date:
  • Size: 8.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.1 CPython/3.10.5

File hashes

Hashes for portfiel-0.1.4.tar.gz
Algorithm Hash digest
SHA256 f6d82de21a9bdecb231a0b59008384a7cbf22dac96c711277b20595ac6df47f0
MD5 9f5ee7ec5a893e030a1265786a4bd642
BLAKE2b-256 3765eefcc69e7f834b1c636000da1b842dbeba111db7783297e9540d5fcf1d7c

See more details on using hashes here.

File details

Details for the file portfiel-0.1.4-py3-none-any.whl.

File metadata

  • Download URL: portfiel-0.1.4-py3-none-any.whl
  • Upload date:
  • Size: 7.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.1 CPython/3.10.5

File hashes

Hashes for portfiel-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 5fa91fe6715c1ca74efe5de8823dc8ce64f2f8325b263e4044c088db404d3df8
MD5 0c1c92c65944b88f9273f141317d8be3
BLAKE2b-256 ac55319d38f258051775617aca4bb8073f599a3e26a900b6c25ae17a90e0bfd9

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page