Portefeuille efficient de Markowitz

J'importe les bibliothèques nécéssaires

In [ ]:
import pandas as pd
import numpy as np
import yfinance as yf
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split

Je crée les variables qui contiennent les dates de début et de fin. Je crée un objet contenant l'ensemble des titres que je souhaite ajouter à mon portefeuille.

PS : Depuis 2017, l'API Yahoo Finance a été abandonnée. Il existe d'autres bibliothèques pour importer les cours historiques des titres financiers.

In [ ]:
DATE_DEBUT = '2015-01-01'
DATE_FIN = '2022-12-31'
tickers = ['AAPL', 'MSFT', 'META', 'GOOG', 'AMZN', 'TSLA', 'NVDA', 'AMD', 'IBM', 'TM', 'DIS']
actif_sans_risque = 3.5/100

J'importe les cours historique à l'aide de la bibliothèque yfinance, je choisis de ne conserver pour chacun des titres que le cours de cloture ajusté.

In [ ]:
stocks = yf.download(tickers, start=DATE_DEBUT, end=DATE_FIN, interval='1wk')
stocks = stocks['Adj Close']
stocks.head()
duree = len(stocks)
[*********************100%***********************]  11 of 11 completed

Je divise mon échantillon en deux parties, je me retrouve avec des données d'entrainements, qui correspondent aux premiers 50% des données initiales.

In [ ]:
train_data, test_data = train_test_split(stocks, test_size=0.5, shuffle=False)
In [ ]:
data = train_data

J'affiche un graphique de mes données

In [ ]:
data.plot(figsize=(10,7))
plt.title('Cours des actions')
plt.legend(tickers)
plt.show()

Vous remarquerez ,sur le graphique ci-dessus, des données manquantes. Cela correspond au fait qu'au moment où ce document a été rédigé, l'API yfinance avait déjà été abandonnée.

Je calcule le rendement de mon portefeuille. cela consiste à calculer le taux de variation $$ TV = \frac{V_2 - V_1}{V_1} \times 100% $$

In [ ]:
rendement = data.pct_change().dropna()
In [ ]:
rendement.plot(figsize=(10,7), title="Rendement journalier")
plt.legend(tickers)
plt.show()

Je calcule ma Matrice de variances covariances sur les rendements $\Sigma$

In [ ]:
cov_matrix = rendement.cov() * duree
cov_matrix
Out[ ]:
AAPL AMD AMZN DIS GOOG IBM META MSFT NVDA TM TSLA
AAPL 0.382149 0.227132 0.213258 0.099244 0.201026 0.086328 0.177000 0.183870 0.277617 0.082692 0.207835
AMD 0.227132 3.075550 0.502631 0.238326 0.151108 0.378132 0.182866 0.275887 0.878926 0.244595 0.397604
AMZN 0.213258 0.502631 0.699076 0.176782 0.326876 0.168063 0.326821 0.333101 0.379513 0.109111 0.386064
DIS 0.099244 0.238326 0.176782 0.245055 0.115184 0.095683 0.118435 0.122804 0.164842 0.103824 0.166805
GOOG 0.201026 0.151108 0.326876 0.115184 0.434032 0.106029 0.316756 0.271831 0.285169 0.131781 0.252305
IBM 0.086328 0.378132 0.168063 0.095683 0.106029 0.292996 0.083873 0.142385 0.201307 0.107986 0.168277
META 0.177000 0.182866 0.326821 0.118435 0.316756 0.083873 0.626485 0.230162 0.268226 0.098720 0.322378
MSFT 0.183870 0.275887 0.333101 0.122804 0.271831 0.142385 0.230162 0.384167 0.304882 0.127990 0.264682
NVDA 0.277617 0.878926 0.379513 0.164842 0.285169 0.201307 0.268226 0.304882 1.435761 0.170029 0.320615
TM 0.082692 0.244595 0.109111 0.103824 0.131781 0.107986 0.098720 0.127990 0.170029 0.266494 0.092687
TSLA 0.207835 0.397604 0.386064 0.166805 0.252305 0.168277 0.322378 0.264682 0.320615 0.092687 1.477749

Je calcule et affiche ma matrice des correlations.

ps: j'admets être surpris, dans la mesure ou l'essentiel des titre choisis appartiennent aux mêmes secteur d'activité, soit l'est entrprise du numerique ou de l'automobile. par exemple, la correlation entre le rendement de TESLA, ici noté TSLA et Toyota Motor TM n'est que de 15%.

In [ ]:
corr_matrix = rendement.corr()
corr_matrix


sns.heatmap(corr_matrix, 
            annot=True, 
            cmap="YlGnBu", 
            linewidths=0.3, 
            annot_kws={"size": 8})

plt.xticks(rotation=90)
plt.yticks(rotation=0) 
plt.show()

Je choisi un nombre de portefeuille à générer. j'ai choisi arbitrairement 5000. dans la variable all_titres je stockerais l'ensemble des portefeuille qui seront généré. mes autres varables stockeront les caracteristiques des 5000 portefeuilles généré, c'est à dire, le rendement espéré la volatilité et le ratio de sharpe.

In [ ]:
nb_portefeuille = 5000
all_titres = np.zeros((nb_portefeuille,len(tickers)))
ret_arr = np.zeros(nb_portefeuille)
vol_arr = np.zeros(nb_portefeuille)
sharpe_arr = np.zeros(nb_portefeuille)

Je génère les portefeuilles en faisant varier les allocations de manière aléatoire et en suivant une marche aléatoire. Cependant, je fais attention à ne pas dépasser une allocation totale de 100% pour chaque portefeuille, afin de ne pas avoir recours aux opérations à découvert. Bien sûr, cette contrainte peut être supprimée si nécessaire.

Une fois les portefeuilles générés, je les stocke dans la variable all_titres, puis j'enregistre les caractéristiques de chaque portefeuille (rendement espéré, volatilité et ratio de Sharpe) dans les autres variables correspondantes.

In [ ]:
for x in range(nb_portefeuille):

    titres = np.array(np.random.random(len(tickers)))
    titres /= np.sum(titres)
    
    all_titres[x,:] = titres

    ret_arr[x] = np.sum(rendement.mean() * titres) * duree

    vol_arr[x] = np.sqrt(np.dot(titres.T, np.dot(rendement.cov() * duree , titres)))

    sharpe_arr[x] = (ret_arr[x]-actif_sans_risque)/vol_arr[x]

Nous pouvons maintenant tracer l'ensemble de nos portefeuilles

In [ ]:
plt.figure(figsize=(10,7))
plt.scatter(vol_arr, ret_arr, c=sharpe_arr, cmap='YlGnBu')
plt.xlabel('Volatilité')
plt.ylabel('rendement')
plt.colorbar(label='Ratio de Sharpe')
plt.show()

Je crée deux lots de varables qui serviront respectivement de coordonnées aux portefeuilles de marché et à variance minimal

In [ ]:
max_sr_vol = vol_arr[sharpe_arr.argmax()]
max_sr_ret = ret_arr[sharpe_arr.argmax()]

min_vol_ret = ret_arr[vol_arr.argmin()]
min_vol_vol = vol_arr[vol_arr.argmin()]

Je trace à nouveau ma frontière efficiente avec en rouge le portefeuille de marché et en noir le portefeuille à variance minimal

In [ ]:
plt.figure(figsize=(10,7))
plt.scatter(vol_arr, ret_arr, c=sharpe_arr, cmap='YlGnBu')
plt.xlabel('Volatilité')
plt.ylabel('Rendement')
plt.colorbar(label='Ratio de Sharpe')
plt.scatter(max_sr_vol, max_sr_ret,c='red', s=50, edgecolors='black')
plt.scatter(min_vol_vol, min_vol_ret,c='Black', s=50, edgecolors='black')
plt.show()

Je récupère l'allocation de mes deux portefeuilles puis je les affiche.

In [ ]:
max_sr_titres = all_titres[sharpe_arr.argmax(),:]

min_vol_titres = all_titres[vol_arr.argmin(),:]
In [ ]:
print('Portefeuille de marché\n')
print('Volatilité du portefeuille :', str(round(max_sr_vol*100, 2)) + ' %')
print('Rendement attendu du portefeuille :', str(round(max_sr_ret*100, 2)) + ' %')
print('Ratio Sharpe du portefeuille :', str(round(sharpe_arr[sharpe_arr.argmax()]*100, 2)) + ' %')

allocation_max = pd.DataFrame({'Titre': data.columns, 'Allocation': max_sr_titres})

allocation_max['Allocation'] = (allocation_max['Allocation']).apply(lambda x: "{:.2%}".format(x).replace('%', ' %'))

print('Allocation optimale :')
print(str(allocation_max))
Portefeuille de marché

Volatilité du portefeuille : 65.74 %
Rendement attendu du portefeuille : 310.59 %
Ratio Sharpe du portefeuille : 467.12 %
Allocation optimale :
   Titre Allocation
0   AAPL     4.03 %
1    AMD    13.33 %
2   AMZN    20.91 %
3    DIS     1.04 %
4   GOOG     5.88 %
5    IBM     0.52 %
6   META    22.80 %
7   MSFT     4.07 %
8   NVDA    19.15 %
9     TM     7.44 %
10  TSLA     0.82 %
In [ ]:
print('Portefeuille à variance minimal\n')
print('Volatilité du portefeuille :', str(round(min_vol_vol*100, 2)) + ' %')
print('Rendement attendu du portefeuille :', str(round(min_vol_ret*100, 2)) + ' %')
print('Ratio Sharpe du portefeuille :', str(round(sharpe_arr[vol_arr.argmin()]*100, 2)) + ' %')

allocation_min = pd.DataFrame({'Titre': data.columns, 'Allocation': min_vol_titres})

allocation_min['Allocation'] = (allocation_min['Allocation']).apply(lambda x: "{:.2%}".format(x).replace('%', ' %'))

print('Allocation optimale :')
print(str(allocation_min))
Portefeuille à variance minimal

Volatilité du portefeuille : 41.24 %
Rendement attendu du portefeuille : 102.27 %
Ratio Sharpe du portefeuille : 239.5 %
Allocation optimale :
   Titre Allocation
0   AAPL    14.74 %
1    AMD     0.10 %
2   AMZN     1.20 %
3    DIS    14.20 %
4   GOOG    13.22 %
5    IBM    21.22 %
6   META     3.19 %
7   MSFT    15.97 %
8   NVDA     1.73 %
9     TM    13.16 %
10  TSLA     1.27 %

Je vais maintenant tester le rendement que l'on aurait obtenu en investissant sur notre portefeuille. Pour ce faire, je vais me servir de mes données de test. L'idée ici est donc de tester notre portefeuille sur des données qu'il n'a jamais vues.

Je conserve la première valeur du dataframe test_data comme cours d'achat et la dernière comme cours de vente. Je définis aussi un capital K.

In [ ]:
K = 10000

achat = test_data.head(1).reset_index(drop=True)
vente = test_data.tail(1).reset_index(drop=True)

L'algorithme suivant simule l'achat des titres en suivant la repartition, puis en revandant à la date d'échéance. $\\ \\ montant\ investi = Allocation \times K\\$ $\ nombre\ parts = \frac{montant\ investi}{achat}\$ $\ montant\ obtenu = nombre\ parts \times vente\$

In [ ]:
montant_investi = max_sr_titres * K

nombre_parts = montant_investi / achat

montant_obtenu = nombre_parts * vente
montant_obtenu
Out[ ]:
AAPL AMD AMZN DIS GOOG IBM META MSFT NVDA TM TSLA
0 1400.303807 4376.777964 2086.387663 82.57006 965.758834 76.504239 1859.748869 967.4644 7577.328198 808.559269 437.51392

Je somme le vecteur les gains par titre contenu dans montant_obtenu

In [ ]:
montant_final = montant_obtenu.sum(axis=1)
montant_final
Out[ ]:
0    25920.934328
dtype: float64

Pour un capital K de 10 000€ le rendement absolue est de 15 920€ la durée séparant la date d'achat et de vente est de 1447 jours soit près de quatre ans.

In [ ]:
# 4. Gain total
gain = montant_obtenu.sum(axis=1) - K

print("Gain obtenu:", gain)
Gain obtenu: 0    15920.934328
dtype: float64

Le rendement est donc de 159% environs pour un placement d'une durée de quatre ans.

In [ ]:
gain/K*100
Out[ ]:
0    159.209343
dtype: float64

quelques pistes d'ameliorations:

  • Utilisation d'une autre API que yahoo finance, les donnée manquante biaise les resultats.
  • Tester l'algorithme sur de très courtes periodes, par exemple 1 jour, 1 minute, 1 seconde.
  • Tester l'algorithme sur des données d'entrainement plus longues, par exemple 1 an.
  • Ajouter davantage de titre, si ce n'est tout les titre coté sur le NASDAQ ou CAC.
  • Ajouter des contraintes, par exemple, ne pas investir dans des titres qui ne sont pas coté sur le même marché.
  • Faire varier le nombre de portefeuille généré, plus il y en a plus la frontière efficiente sera précise.

De façon plus général, il serait de bonne augure de faire evoluer ce model. Je pourrait utiliser le CAPM pour mesure le rendement attendu d'un titre, et ainsi améliorer la frontière efficiente. Je pourrais aussi utiliser un model garch pour mesurer le risque d'un titre. je pourrais aussi faire evoluer le model de markowitz en utilisant le portefeuille de Black-Litterman.