Blogs

Portfolio Optimization

The optimization is based on historical data. The result doesn't mean it is the optimized portfolio for the future.I n fact, It's very likely that it isn't.

Start with imports (use pip install pandas-datareader if it's not installed):

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import pandas_datareader.data as pdr

Get selected coin historical data from Yahoo Finance (free). These three coin is specificly selected for this example after a few trial and error. They form a good portfolio optimization example. Clearly, ot all combinations of coins and dates will give similar results. 

data = pdr.DataReader(['BTC-USD','ETH-USD','BNB-USD',], 'yahoo', start='2021-06-12', end='2021-12-30')
data = data.Close
dly_return = data.pct_change().iloc[1:]

The result is a simple dataframe with daily percentage returns:

Create randomly weighted portfolios and calculate their performances:

port_ret, port_std, port_weight, sharpe = [], [], [], []
num_asset = len(dly_return.columns)
num_port = 500
np.random.seed(101)
dly_cov = dly_return.cov()

for _ in range(num_port):
    rand_weights = np.random.random(num_asset)
    rand_weights = rand_weights / np.sum(rand_weights)
    port_weight.append(rand_weights)

    rand_returns = np.sum(dly_return.mean() * rand_weights)*365
    port_ret.append(rand_returns)

    rand_std = np.sqrt(np.dot(rand_weights.T, np.dot(dly_cov*365, rand_weights)))
    port_std.append(rand_std.mean())

    rand_sharpe = (rand_returns.mean() / rand_std.mean())
    sharpe.append(rand_sharpe)
    
portfolio = {'Return': port_ret,
             'Std': port_std,
             'Sharpe': sharpe}

for i, symbol in enumerate(dly_return.columns):
    portfolio[symbol+'_weight'] = [Weight[i] for Weight in port_weight]

portfolio_df = pd.DataFrame(portfolio)
column_order = ['Return', 'Std', 'Sharpe'] + [symbol+'_weight' for symbol in dly_return.columns]
portfolio_df = portfolio_df[column_order]

The resulted dataframe looks like:

Next, find optimum retururn, minimum std portfolio weights based on risk levels: 

max_sharpe_port = portfolio_df[portfolio_df['Std'] == portfolio_df['Std'].min()]
min_std_port = portfolio_df[portfolio_df['Sharpe'] == portfolio_df['Sharpe'].max()]
target_std_interval = [1, 0] # select your target risk level to see the best return portfolio
maxret_by_targetstd = portfolio_df[(portfolio_df.Std < target_std_interval[0]) & (portfolio_df.Std > target_std_interval[1])].sort_values(by='Return',ascending=False).iloc[0]

Min Std Portfolio:

Return            1.121176
Std               0.802215
Sharpe            1.397600
BTC-USD_weight    0.004903
ETH-USD_weight    0.586632
BNB-USD_weight    0.408465

Max Sharpe Portfolio:

Return            0.763374
Std               0.681481
Sharpe            1.120169
BTC-USD_weight    0.865129
ETH-USD_weight    0.021218
BNB-USD_weight    0.113653

Max Return Withn 1 Std Portfilo:

Return            1.146073
Std               0.831619
Sharpe            1.378123
BTC-USD_weight    0.002784
ETH-USD_weight    0.916076
BNB-USD_weight    0.081140

 

Visualize all portfolios with spotted Max Sharpe and Min Std:

portfolio_df.plot.scatter(x='Std', y='Return',
                          c=portfolio_df['Sharpe'], cmap='YlGnBu', edgecolors='black', grid=True)

plt.scatter(x=max_sharpe_port['Std'], y=max_sharpe_port['Return'],
            c='red', marker='D', s=200)

plt.scatter(x=min_std_port['Std'], y=min_std_port['Return'],
            c='blue', marker='D', s=200)

plt.xlabel('Port Std Dly')
plt.ylabel('Avg Returns Dly')
plt.title('Coin Portfolio Performances')
plt.show()

 

Find all optimum portfolios:

from scipy.optimize import minimize

def get_ret_vol_sr(weights):
    weights = np.array(weights)
    ret = np.sum(dly_return.mean() * weights) * 365
    std = np.sqrt(np.dot(weights.T, np.dot(dly_return.cov() * 365, weights)))
    sr = ret/std
    return np.array([ret, std, sr])

def portfolio_std(weights):
    return get_ret_vol_sr(weights)[1]

def portfolio_return(weights):
    return  get_ret_vol_sr(weights)[0]


bounds = tuple((0,1) for asset in range(len(dly_return.columns)))
init_guess = len(dly_return.columns)*[1./len(dly_return.columns),]
target_returns = np.linspace(0.8, 1.1, 10)
frontier_volatility = []

for target in target_returns:
    cons = ({'type':'eq','fun': lambda w: np.sum(w) - 1},
            {'type':'eq','fun': lambda w: portfolio_return(w) - target})

    result = minimize(portfolio_std, init_guess, method = 'SLSQP',
                         bounds = bounds,constraints = cons)

    frontier_volatility.append(result['fun'])

Visualize all optimum portoflios as the efficient frotier:

portfolio_df.plot.scatter(x='Std', y='Return', edgecolors='black', figsize=(10,8), grid=True)
plt.scatter(x=max_sharpe_port['Std'], y=max_sharpe_port['Return'],
            c='red', marker='D', s=200)
plt.scatter(x=min_std_port['Std'], y=min_std_port['Return'],
            c='blue', marker='D', s=200)
plt.plot(frontier_volatility, target_returns, 'g--', linewidth = 3)
plt.xlabel('Port Std Dly)')
plt.ylabel('Avg Returns Dly')
plt.title('Coin Portfolio Performances')
plt.show()