Portfolio optimization using Python
Portfolio optimization using Python involves using mathematical and computational techniques to construct an investment portfolio that aims to maximize returns for a given level of risk or minimize risk for a desired level of return. Python, with its extensive libraries for data analysis, optimization, and visualization, offers a powerful platform for performing these analyses.
Portfolio optimization using Python combines finance theory, quantitative analysis, and computational methods to construct portfolios that balance risk and return according to specific investment objectives. Python’s flexibility and powerful libraries make it a popular choice for portfolio managers, analysts, and researchers in the finance domain.
We will analyze a portfolio of stocks containing Tech Mahindra, Aurobindo Pharma, TATA Consumer Products and Havells. The stocks have been chosen randomly from the different sectoral NIFTY indices. The stock data for these stocks have been taken from Yahoo finance.
Before performing the analysis following steps were taken to prepare the data for analysis:
- The required python libraries like Numpy, Pandas, Matplotlib, and Seaborn were imported.
- The stock data obtained from Yahoo finance was read into the dataframe with company name using read_csv method.
- Names of the stock were stored in a list to be used further in the code
- The date column in the data obtained from Yahoo finance was changed to datetime for pandas library to recognize it as date and set as the index column.
The code for the above steps is shown below.
The separated dataframes containing stock data for the four companies should be concatenated into one dataframe containing all the stock data. This is done using the code shown below using pandas concat function.
In the code shown above in the two images, a separate dataframe called “data” is created by concatenating the four dataframes containing stock data for the four companies. Also, the columns names, “Company”, “Info” are set for the new concatenated dataframe in the code in the next image.
Let us calculated the daily returns for the four stocks in a new dataframe called “returns”. Daily returns are calculated as the percent change in the daily prices. The calculation in Python is shown below.
The code in the image above shows the calculation of daily returns using a for loop for each stock in the concatenated dataframe “data”. The daily percent changes is calculated using the .pct_change(method) in a separate dataframe “returns”.
Using the daily returns, the average annual returns and covariance is calculated as shown in the code below.
The code above shows the calculation of average annual returns and annual covariance. The covariance is calculated by using the .cov() method which gives a covariance matrix. The daily returns and covariance are annualized by multiplying it by 250, since there are around 250 trading days in a year.
The daily returns calculated are visualized using a histogram with the daily returns on the x-axis as shown in the code below.
The distribution of the returns for each stock can be seen in the image above.
Let us now move on to the portfolio optimization part. First we will optimize the allocation to the stocks of the portfolio using Monte Carlo simulation. Monte Carlo simulation is a powerful technique used in portfolio optimization to assess the potential outcomes of different investment strategies or different allocations under varying conditions. It involves generating multiple scenarios based on statistical models and random sampling.
Implementing Monte Carlo simulation in Python involves combining statistical analysis, simulation, and optimization techniques to gain insights into portfolio performance under different allocations. For our analysis, we will run a simulation on different allocations of the same stocks to find the optimum allocation. A single run of the simulation is shown in the code below.
The code demonstrates a single run of a basic portfolio optimization process using Python and NumPy. It demonstrates a basic single-run scenario of portfolio optimization, calculating the key metrics of return, volatility, and the Sharpe Ratio for a portfolio with randomly generated initial weights. There are several steps involved, which are as follows:
- np.random.random(4) generates an array of random numbers representing initial weights for each asset in the portfolio.
- Rebalancing Weights: Normalizing the randomly generated weights to ensure they sum up to 1, representing a fully invested portfolio.
- Portfolio Return: Calculating the portfolio’s expected return using the weighted average of individual asset returns. It multiplies the annual mean returns of each asset by its respective weight and aggregates them.
- Expected Volatility (Standard Deviation): Using the formula for portfolio volatility, it calculates the square root of the dot product of weights, the covariance matrix of asset returns (multiplied by 250 for annualization), and weights transpose. The square root of covariance matrix is taken because square root of variance is standard deviation or volatility.
- Sharpe Ratio: Finally, it computes the Sharpe Ratio, which measures the risk-adjusted return by subtracting the risk-free rate (assumed as 7.5% as the yield of 10 year Indian Government bond) from the portfolio return and dividing by its volatility.
Having seen a single run of the simulation above, let us now perform 5000 simulations of the random allocations generated to find the optimum allocation for the four stocks chosen. The portfolio performance will analyzed based on the Sharpe ratio. The Sharpe ratio gives the return delivered per unit of risk taken. The code for the simulation is shown below.
The code runs a Monte Carlo simulation to optimize portfolios. The steps involved are as follows:
- num_ports = 5000: It specifies the number of simulated portfolios to generate (in our case, 5000).
- all_weights: A 2D array to store the randomly generated weights for each asset in each portfolio.
- ret_arr, vol_arr, sharpe_arr: Arrays to store portfolio returns, volatilities, and Sharpe ratios for each simulated portfolio.
- for i in range(num_ports): Looping over each portfolio to perform the following steps:
- Generating Random Weights: Randomly assigning weights to each asset in the portfolio and rebalancing them to sum up to 1 (representing 100% allocation).
- Calculating Portfolio Metrics: Computing the expected return (ret_arr), volatility (vol_arr), and subsequently, the Sharpe ratio (sharpe_arr) for each portfolio based on the randomly generated weights.
After performing the above 5000 simulations, we see the the maximum Sharpe ratio for the simulated allocations. We can see that the highest Sharpe ratio obtained is 0.75 for the portfolio of stocks. Using the index position of this Sharpe ratio we can see the allocations for this highest Sharpe ratio. The allocations are around 18.6 % for Tech Mahindra, 0.6% for Aurobindo (almost zero allocation), 59.9 % for TATA Consumer Products, 20.07 % for Havells.
Let us plot the volatility, return and sharpe ratio values for the simulation. The code and the visualization can be seen below.
The visualization can be seen above for volatility, return and sharpe ratio. The x-axis shows the volatility, y axis shows the return and the colorbar on right shows the sharpe ratio graded colorwise with dark color showing the lowest and light values showing the highest sharpe ratio. The red dot shows the highest Sharpe ratio point on the plot.
We have seen how to apply Monte Carlo simulation for creating random allocation values and analyzing the portfolios for those allocations to find the optimum allocation. This method is quite useful and efficient. But if we a vast number of stocks, like hundreds in our portfolio, running random allocation simulation might take too long or require a lot of processing. For this, we can use the mathematical optimization to get the job done.
In our next analysis method, we will optimize the same portfolio allocation mathematically using the minimize function in Scipy (a library in Python) and Sharpe ratio. Portfolio optimization using Scipy’s minimize function and the Sharpe ratio involves using mathematical optimization to find the optimal asset allocation that maximizes the Sharpe ratio—a measure of risk-adjusted returns. The basic principle is to find the Sharpe ratio for a random allocation and then multiply it by -1 to make it negative and then minimize it to obtain the allocation weights that gives the highest Sharpe ratio. The code is shown below.
The code above shows portfolio optimization using scipy’s minimize function. The code first defines a function ret_vol_sr that calculates the portfolio's return, volatility, and Sharpe ratio given a set of asset weights. Another function is created that computes the negative Sharpe ratio by multiplying it by -1.
Then, the constraints for the optimization are defined. First the sum_check function checks that the sum of weights equals 1 by calculating the difference of sum of weights and 1, which should be zero. Then, the bounds are created which makes sure that the weights allocated are between 0 and 1 for the four allocations. Lastly, an initial guess is taken to start. By conventions, an equal allocation is assumed. The results of the optimization are stored in a variable called opt_results (short for optimum results)
After running the optimization, the results can be seen below.
The code above shows the results for the optimization. The values of the optimum allocation are stored in x in the opt_results variable. The opt_results.x shows the allocation to be around 15.68 % for Tech Mahindra, 0 % for Aurobindo, 61 % for TATA Consumer Products, and 23.2 % for Havells. For this optimum allocation obtained, we can see the returns, volatility and Sharpe ratio obtained by using the ret_vol_sr function that we created earlier. It can be seen that the highest Sharpe ratio obtained is 0.76, returns are 22.5 % and volatility is 19.69 % for this optimum allocation.
This brings us to the end of portfolio optimization using Python. We looked at both the ways of optimizing portfolio allocation. First, by running a Monte Carlo simulation of the portfolio allocation weights and analyzing it. Second, by using mathematical optimization to come up with optimum allocation for the stocks. However, the application of methods should be chosen keeping the desired outcome and complexity in mind.