Skip to content

Latest commit

 

History

History
392 lines (299 loc) · 12.5 KB

README.md

File metadata and controls

392 lines (299 loc) · 12.5 KB

Visit QuantNet

Visit QuantNet FRM-EM-paper Visit QuantNet 2.0

Name of Quantlet: FRM-EM-paper

Published in: ''

Description: 'Uplifted hierarchical risk parity based FRM portfolio construction.'

Keywords: 'FRM (Financial Risk Meter), Emerging Markets, Uplifted Hierarchical Risk Parity, Inverse lambda, Min Variance'

Author: 'Souhir Ben Amor'

See also: ''

Submitted:  '08.02.2021'

Additional Info : 'This repository implements the upHRP algorithm, the MinVAr portfolio, and the InvLamda portfolio.
The repository includes a code for the classical portfolio optimisation approaches (MinVar, IV, and HRP),
and another code for the Uplifted portfolio optimisation approaches upHRP and the InvLambda. 
The input data, as well as the output, are also provided.
Code for classical HRP is based on Lopez de Prado, M. (2018). Advances in Financial 
Machine Learning. Wiley. The code has been modified to create an uplifted portfolio
strategies based on FRM (Financial Risk Meter) adjacency matrices and it is adapted to be used with
python 3 and the data set.'

Picture1

Picture2

Picture3

Picture4

Picture5

Picture6

Picture7

Picture8

Picture9

Picture10

Picture11

Picture12

Picture13

PYTHON Code

#This file implements the upHRP algorithm, the MinVAr portfolio and the InvLamda portf.
#Code for classical HRP is based on Lopez de Prado, M. (2018). Advances in Financial 
#Machine Learning. Wiley. The code has been modified to create an uplifted portfolio
#strategies based on FRM adjacency mtrices and its adapted in order to be used with
#python 3 and the data set.
 
#Souhir Ben Amor
#@date: 20201010
#"""

#[0] Import library
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

#[0]Upload input data, Financial Institutions of the 6 Emerging Markets and adjacency matrix
FIs_prices = pd.read_excel("Financial Institutions Price Series.xlsx")
FRM_EM_Adjacency_matrix= pd.read_csv("adj_matix_20200630_050.csv")
print(FIs_prices)

# In[1]: 
# Load modules

import os
path = os.getcwd() # Set Working directory here

# Import modules for Datastructuring and calc.
import pandas as pd
import numpy as np
from scipy import stats
import warnings
from tqdm import tqdm

# Modules for RHP algorithm
import matplotlib.pyplot as mpl
import scipy.cluster.hierarchy as sch

# Modules for the network plot
import networkx as nx
from networkx.convert_matrix import from_numpy_matrix

# Modules for Markowitz optimization
import cvxopt as opt
import cvxopt.solvers as optsolvers

warnings.filterwarnings("ignore") # suppress warnings in clustering

# In[2]: 
# define functions for HRP and IVP

def getIVP(cov,**kargs):
    # Compute the inverse-variance portfolio
    ivp=1./np.diag(cov)
    ivp/=ivp.sum()
    return ivp


def getClusterVar(cov, cItems):
    # Compute variance per cluster
    cov_=cov.loc[cItems, cItems] # matrix slice
    w_=getIVP(cov_).reshape(-1,1)
    cVar=np.dot(np.dot(w_.T,cov_),w_)[0,0]
    return cVar


def getQuasiDiag(link):
    # Sort clustered items by distance
    link=link.astype(int)
    sortIx=pd.Series([link[-1,0],link[-1,1]])
    numItems=link[-1,3] # number of original items
    while sortIx.max() >=numItems:
        sortIx.index=range(0,sortIx.shape[0]*2,2) # make space
        df0=sortIx[sortIx>=numItems] # find clusters
        i=df0.index;j=df0.values-numItems
        sortIx[i]=link[j,0] # item 1
        df0=pd.Series(link[j,1], index=i+1)
        sortIx=sortIx.append(df0) # item 2
        sortIx=sortIx.sort_index() # re-sort
        sortIx.index=range(sortIx.shape[0]) # re-index
    return sortIx.tolist()


def getRecBipart(cov,sortIx):
    # Compute HRP alloc
    w=pd.Series(1,index=sortIx)
    cItems=[sortIx] # initialize all items in one cluster
    while len(cItems)>0:
        cItems=[i[j:k] for i in cItems for j,k in ((0,len(i)//2),(len(i)//2,\
                len(i))) if len(i)>1] # bi-section
        for i in range(0,len(cItems),2): # parse in pairs
            cItems0=cItems[i] # cluster 1
            cItems1=cItems[i+1] # cluster 2
            cVar0=getClusterVar(cov,cItems0)
            cVar1=getClusterVar(cov,cItems1)
            alpha=1-cVar0/(cVar0+cVar1)
            w[cItems0]*=alpha # weight 1
            w[cItems1]*=1-alpha # weight 2
    return w


def correlDist(corr):
    # A distance matrix based on correlation, where 0<=d[i,j]<=1 
    # This is a proper distance metric
    dist=((1-corr)/2.)**.5 # distance matrix
    return dist


def plotCorrMatrix(path, corr, labels=None):
    # Heatmap of the correlation matrix
    if labels is None:labels=[]
    mpl.pcolor(corr)
    mpl.colorbar()
    mpl.yticks(np.arange(.5,corr.shape[0]+.5),labels)
    mpl.xticks(np.arange(.5,corr.shape[0]+.5),labels)
    mpl.savefig(path,dpi=300, transparent=True)
    mpl.clf();mpl.close() # reset pylab
    return

# In[3]: 
# define function for MinVar portfolio
    
# The MIT License (MIT)
#
# Copyright (c) 2015 Christian Zielinski
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.   

def min_var_portfolio(cov_mat, allow_short=False):
    """
    Computes the minimum variance portfolio.

    Note: As the variance is not invariant with respect
    to leverage, it is not possible to construct non-trivial
    market neutral minimum variance portfolios. This is because
    the variance approaches zero with decreasing leverage,
    i.e. the market neutral portfolio with minimum variance
    is not invested at all.
    
    Parameters
    ----------
    cov_mat: pandas.DataFrame
        Covariance matrix of asset returns.
    allow_short: bool, optional
        If 'False' construct a long-only portfolio.
        If 'True' allow shorting, i.e. negative weights.

    Returns
    -------
    weights: pandas.Series
        Optimal asset weights.
    """
    if not isinstance(cov_mat, pd.DataFrame):
        raise ValueError("Covariance matrix is not a DataFrame")

    n = len(cov_mat)
        
    P = opt.matrix(cov_mat.values)
    q = opt.matrix(0.0, (n, 1))

# Constraints Gx <= h
    if not allow_short:
    # x >= 0
       G = opt.matrix(-np.identity(n))
       h = opt.matrix(0.0, (n, 1))
    else:
        G = None
        h = None

# Constraints Ax = b
# sum(x) = 1
    A = opt.matrix(1.0, (1, n))
    b = opt.matrix(1.0)

    # Solve
    optsolvers.options['show_progress'] = False
    sol = optsolvers.qp(P, q, G, h, A, b)
        
    if sol['status'] != 'optimal':
        warnings.warn("Convergence problem")
            
 # Put weights into a labeled series
    weights = pd.Series(sol['x'], index=cov_mat.index)
    return weights  

# In[4]: 
# Define functions for network graphs
    
#Function to plot Network plots
def plotNetwork(path,corr):
    # Transform it in a links data frame
    #links=corr.stack().reset_index()
    #Build graph
    corr=Corr_mat
    adj_matrix = corr
    constits_latest = corr.index
    # remove self-loops
    adj_matrix = np.where((adj_matrix<=1.000001) & (adj_matrix>=0.99999),0,adj_matrix)
    # replace values that are below threshold
    # create undirected graph from adj_matrix
    graph = from_numpy_matrix(adj_matrix, parallel_edges=False, create_using= nx.Graph())
    # set names to crypots
    graph = nx.relabel.relabel_nodes(graph, dict(zip(range(len(constits_latest)), constits_latest)))
    pos_og =  nx.circular_layout(graph, scale=2)
    pos = nx.circular_layout(graph, scale=1.7)
    
    for p in pos:  # raise text positions
        if pos[p][1]>1:
            pos[p][1] += 0.15
        if pos[p][1]<-1:
            pos[p][1] -= 0.15
        elif pos[p][0]<0:
            pos[p][0] -= 0.3
        else:
            pos[p][0]+=0.3
    plt = mpl.figure(figsize = (5,5)) 
    nx.draw(graph, pos_og, with_labels= False)
    nx.draw_networkx_labels(graph, pos)
     
    plt.savefig(path,dpi=300 ,transparent=True)
    mpl.clf();mpl.close()
    return

## In[5]:
# Loading and structuring crypto data sets

FIs_prices = FIs_prices[(~FIs_prices.isnull()).all(axis=1)] # Deleting empty rows
FIs_prices = FIs_prices.rename(columns = {"date":"Date"})
FIs_prices = FIs_prices.replace(to_replace = 0, method = "ffill")
Price_data_univ=FIs_prices
Price_data_univ = Price_data_univ.set_index("Date") # define Date  as index
# Calculating returns 
Return_data_univ = Price_data_univ.pct_change() #calculate daily returns
Return_data_univ = Return_data_univ.drop(Return_data_univ.index[range(0,1)])

Cov_mat1 = Return_data_univ.cov() # Covariance matrix of the return matrix
Corr_mat1=Return_data_univ.corr() # Correlation matrix of the return matrix
FRM_EM_Adjacency_matrix = FRM_EM_Adjacency_matrix.rename(columns = {"date":""})
FRM_EM_Adjacency_matrix = FRM_EM_Adjacency_matrix.set_index("") # define Date  as index
Corr_mat=FRM_EM_Adjacency_matrix
Cov_mat=FRM_EM_Adjacency_matrix

# In[6]:
# Heatmap and network analysis of corr. matrix

# Plotting Correlation matrix heatmap

plotCorrMatrix(path+"/Adj_matrix_Heatmap_FIs_unsorted",Corr_mat)


# network plot of correlation matrix

plotNetwork(path+"/Corr_Network_FIs_unsorted.png", Corr_mat)

# Sort correlation matrix
dist=correlDist(Corr_mat)
link=sch.linkage(dist,'single')
sortIx=getQuasiDiag(link) 
sortIx=Corr_mat.index[sortIx].tolist() # recover labels 
Corr_sorted=Corr_mat.loc[sortIx,sortIx] # reorder

# Plot sorted correlation matrix
plotCorrMatrix(path+"/Adj_matrix_Heatmap_FIs_sorted",Corr_sorted)

# Plot dendogram of the constituents
#2) Cluster Data
mpl.figure(num=None, figsize=(20, 10), dpi=300, facecolor='w', edgecolor='k')    
dn = sch.dendrogram(link, labels = dist.columns)
mpl.savefig(path+"/Dendrogram_FIs.png", transparent = True, dpi = 300)
mpl.clf();mpl.close() # reset pylab

print(plotNetwork)

# In[7]:
#Function to calculate the HRP portfolio weights

def HRPportf(cov,corr):
    #1) Cluster covariance matrix
    dist=correlDist(corr)
    link=sch.linkage(dist,'single')
    sortIx=getQuasiDiag(link) 
    sortIx=corr.index[sortIx].tolist() # recover labels
    #2) Allocate capital according to HRP
    weights_hrp=getRecBipart(cov,sortIx)
    return weights_hrp


# In[8]:
# Compute the weights for the Markowitz MinVar and the HRP portfolio and the 
# IVP portfolio

w_HRP=np.array([HRPportf(Cov_mat1,Corr_mat1).index,HRPportf(Cov_mat,Corr_mat).round(3)])
w_HRP=pd.DataFrame(np.transpose(w_HRP))
w_HRP.columns = ["Asset","Weights HRP"]

w_MinVar= np.array([min_var_portfolio(Cov_mat1).index,min_var_portfolio(Cov_mat1).round(3)])
w_MinVar=pd.DataFrame(np.transpose(w_MinVar))
w_MinVar.columns = ["Asset","Weights MinVar"]

w_IVP= np.array([Cov_mat1.index, getIVP(Cov_mat).round(3)])
w_IVP=pd.DataFrame(np.transpose(w_IVP))
w_IVP.columns = ["Asset","Weights IVP"]

Weights = pd.merge(w_MinVar,w_IVP,\
                   on="Asset", how = "inner")
Weights = pd.merge(Weights,w_HRP,\
                   on="Asset", how = "inner")

print(Weights.to_latex(index=True)) # Latex table output


automatically created on 2021-02-11