avatarAlexzap

Summary

The provided content outlines a comprehensive analysis of a risk-adjusted portfolio optimization strategy involving Bitcoin, Gold, Oil, and EUR/USD, utilizing advanced quantitative trading techniques and statistical models to maximize the risk-adjusted return.

Abstract

The article delves into a sophisticated quantitative approach for optimizing a portfolio that includes Bitcoin (BTC), Gold, Crude Oil, and the EUR/USD exchange rate. It employs a range of statistical and machine learning techniques, including AutoEDA, Scipy SLSQP, Markowitz's portfolio theory, and the Sharpe ratio, to construct a portfolio that aims to maximize the return-to-risk ratio. The study investigates the interconnectedness of these financial assets and their potential as hedge and safe-haven assets against stock market volatility. It also discusses the methodology for importing packages, performing automated EDA reports, handling missing data, converting currencies, and plotting log returns. The analysis includes time series analysis, such as ACF/PACF, ADF tests, and the fitting of ARIMA models, to ensure the stationarity of the data. The article further explores the use of Monte Carlo simulations to visualize the efficient frontier and the application of the Scipy SLSQP optimization algorithm to determine the optimal portfolio weights. The final comparison includes the efficient frontier, the tangency portfolio, and the minimum variance portfolio, with conclusions drawn on the diversification capabilities and risk-adjusted performance of the proposed portfolio.

Opinions

  • The article suggests that Bitcoin, despite its high volatility, can be part of a diversified investment portfolio that seeks to maximize risk-adjusted returns.
  • Gold is considered to have a low risk and is seen as a traditional safe-haven asset, while oil is recognized for its high volatility and lower return.
  • The study posits that a well-diversified portfolio can be constructed using quantitative methods to balance risk and return effectively.
  • The authors express that the Sharpe ratio is a critical metric for evaluating the performance of a portfolio, with a higher Sharpe ratio indicating a more favorable risk-adjusted return.
  • The use of advanced statistical models, such as ARIMA and GARCH, is advocated for understanding and predicting the behavior of financial time series data.
  • The Monte Carlo simulation is presented as a valuable tool for visualizing the range of possible outcomes and for identifying the efficient frontier in portfolio optimization.
  • The article concludes that the maximum Sharpe ratio portfolio may not always align with investor expectations, emphasizing the importance of considering additional risk metrics and investment strategies.

Risk-Adjusted BTC-Gold-Oil- EURUSD Portfolio Optimization for Quant Traders: AutoEDA, Scipy SLSQP, Markowitz, Sharpe & VAR

Risk-Adjusted Quant Trading Roadmap: 7 Steps towards max(ROI/Risk) or min(Risk/ROI)=min(Risk)/max(ROI) Ratio (image template via Canva).
Investment Strategies for Beginners (image template via Canva).
  • Referring to the recent case example of fintech Time Series Analysis (TSA), this article investigates statistically significant relationships between Gold (GC=F), Crude Oil (CL=F), and Bitcoin (BTC) within the confines of risk-adjusted quant trading that maximizes the ROI/Volatility ratio.
  • Specifically, we model the linkages between BTC, gold, EUR, and crude oil using the stochastic portfolio optimization approach.
  • Our analysis also includes the EURUSD exchange rate risk. It accounts for the exposure faced by investors that operate across different countries within EU.

Goals

  • This study investigates whether gold, EUR/USD, oil and BTC are hedge and safe haven assets against stock and if they are useful in diversifying downside risk for international stock markets.
  • Our ultimate goal is to implement the Multi-Objective Portfolio Optimization in Python based upon the TSA tutorial and the recent pilot project.
  • This is a financial adventure through time. It started around the 1950s with a financial wizard named Harry Markowitz (efficient frontier, max Sharpe ratio, min variance, CAL, etc.).
  • The objective typically maximizes factors such as expected return, and minimizes costs like financial risk, resulting in a multi-objective optimization problem.
  • Key Task: download real-time stock data using yfinance to practice TSA (arch, scipy, statsmodels, etc.), GARCH model of residuals, ACF/PACF and Q-Q plot diagnostics, statistical testing (SARIMAX, ADF, Shapiro-Wilks, etc.), model estimation (AIC, BIC, etc.), and MPT in Python.

Motivation

  • BTC has attracted great attention around the world since its introduction in 2008.
  • In fact, BTC is the best-performing asset of the last 10 years.
  • But how has BTC performed against other asset classes such as precious metals and oil more recently?
  • Research indicates that BTC has the potential to replace gold as a hedge against inflation and become a new investment asset, with a strong substitution effect between the two assets.
  • The paper’s motivation is based upon the idea that BTC can be similar to gold in terms of its hedging properties and can be used for hedging for different assets. Moreover, although it is more metaphorical, BTC is also accepted because it is mined like crude oil, viz. a commodity. These similarities can be investigated by analyzing the connectedness among these financial assets in the sequel.

Scope

  • The proposed Risk-Adjusted Quant Trading Roadmap consists of the following 7 Steps geared towards max(ROI/Risk) Ratio:
  • Step 1: Input data loading, editing, pre-processing and Automated Exploratory Data Analysis (Auto EDA) with ydata-profiling & sweetviz.
  • Step 2: Computing rolling mean, std, and log daily returns of our assets.
  • Step 3: Analyzing ACF/PACF, ADF, checking the mean, skewness and kurtosis of log returns.
  • Step 4: Examining the SARIMAX statistical summary and residual plots (Q-Q, ACF, etc.), calculating annualized returns vs volatility, and comparing covariance vs correlations.
  • Step 5: Monte Carlo simulation of random portfolios, plotting the Markowitz efficient frontier vs the Sharpe ratio.
  • Step 6: Stochastic optimization of portfolio weights using the Scipy SLSQP method.
  • Step 7: Final comparison of the risk-adjusted portfolio against the efficient frontier, minimum variance and tangency portfolios that offer the highest expected return for a specific level of risk.
  • Let’s delve into details of this approach implemented in Python 3.12.2.

Importing Packages

  • Setting the working directory and importing packages
import os
os.chdir('YOURPATH')    # Set the working directory
os. getcwd() 
# Import packages
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import statsmodels.graphics.tsaplots as sgt
import statsmodels.tsa.stattools as sts
from statsmodels.tsa.arima_model import ARIMA, ARMA
import statsmodels.api as sm
import statsmodels.tsa.arima_model  
from scipy import stats
from math import sqrt 
import seaborn as sns
import pylab
import scipy
import yfinance
from arch import arch_model
from scipy.stats import shapiro
from statsmodels.stats.diagnostic import het_arch, acorr_ljungbox

#Basic Imports for Optimization 
import math
import scipy.stats as scs
import statsmodels.api as sm
from pylab import mpl, plt
import scipy.optimize as sco
import scipy.interpolate as sci
mpl.rcParams['font.family'] = 'serif'
%matplotlib inline

#AutoEDA
!pip install pandas_profiling, sweetviz
from pandas_profiling import ProfileReport
import sweetviz as sv

# to ignore some warnings in python when fitting models ARMA or ARIMA
import warnings 
warnings.filterwarnings('ignore')

Reading Input Stock Data

  • Reading the Adj Close of BTC-USD, GC=F, CL=F, and EURUSD=X from 2022–01–03 to 2024–05–03
data = yfinance.download(tickers= 'BTC-USD, GC=F, CL=F, EURUSD=X', 
                         start="2022-01-03", end="2024-05-04", 
                         group_by='column')['Adj Close']

data
Ticker BTC-USD CL=F EURUSD=X GC=F
Date    
2022-01-03 46458.117188 76.080002 1.137346 1799.400024
2022-01-04 45897.574219 76.989998 1.130224 1814.000000
2022-01-05 43569.003906 77.849998 1.128363 1824.599976
2022-01-06 43160.929688 79.459999 1.131350 1788.699951
2022-01-07 41557.902344 78.900002 1.129688 1797.000000
... ... ... ... ...
2024-04-29 63841.121094 82.629997 1.070687 2345.399902
2024-04-30 60636.855469 81.930000 1.071570 2291.399902
2024-05-01 58254.011719 79.000000 1.066655 2299.899902
2024-05-02 59123.433594 78.949997 1.072156 2299.199951
2024-05-03 61715.382812 78.580002 1.076426 2301.300049
852 rows × 4 columns
  • Check the number of missing values of the raw data
data.isna().sum()
Ticker
BTC-USD       0
CL=F        264
EURUSD=X    242
GC=F        264
dtype: int64

Automated EDA Reports

  • Creating the Pandas Profiling (aka ydata-profiling) HTML Report
profile = ProfileReport(data, title="Pandas Profiling Report")
profile.to_file("report.html")
  • Creating the sweetviz EDA report
report = sv.analyze(data)

# Display the report
report.show_html()

Data Pre-Processing

  • Filling the missing values
data['EURUSD=X'].fillna(method='bfill', inplace=True) 
data['CL=F'].fillna(method='bfill', inplace=True) 
data['GC=F'].fillna(method='bfill', inplace=True) 
# Check the number of missing values again
data.isna().sum()
Ticker
BTC-USD     0
CL=F        0
EURUSD=X    0
GC=F        0
dtype: int64
  • Performing USD-to-EUR currency conversion
data['btc'] = data['BTC-USD']/data['EURUSD=X']
data['gold'] = data['GC=F']/data['EURUSD=X']
data['oil'] = data['CL=F']/data['EURUSD=X']
  • Creating three DataFrames for further analysis
df1 = data['btc'].to_frame()
df2 = data['gold'].to_frame()
df3 = data['oil'].to_frame()
  • Plotting the daily prices of BTC, Gold and Oil
# Bitcoin prices
df1.btc.plot(figsize=(15,4))
plt.ylabel("EUR")
plt.title('Daily Prices of Bitcoin', fontsize=16)
plt.grid()
plt.show()

# Gold prices
df2.gold.plot(figsize=(15,4))
plt.ylabel("EUR per ounce")
plt.title('Daily Prices of Gold', fontsize=16)
plt.grid()
plt.show()

# Oil prices
df3.oil.plot(figsize=(15,4))
plt.ylabel("EUR per barrel")
plt.title('Daily Prices of Crude Oil', fontsize=16)
plt.grid()
plt.show()
Daily prices of BTC, Gold and Oil

Rolling Mean & STD

  • Calculating and plotting the rolling mean and STD of daily prices
df1['rolling_mean_btc'] = df1.btc.rolling(window=12).mean()
df1['rolling_std_btc'] = df1.btc.rolling(window=12).std()
df1.plot(title='Rolling Mean and Rolling Standard Deviation of Bitcoin Prices', 
         figsize=(15,6))
plt.ylabel("EUR")
plt.grid()
plt.show()
Rolling Mean and Rolling Standard Deviation of Bitcoin Prices
df2['rolling_mean_g'] = df2.gold.rolling(window=12).mean()
df2['rolling_std_g'] = df2.gold.rolling(window=12).std()
df2.plot(title='Rolling Mean and Rolling Standard Deviation of Gold Prices',figsize=(15,5))
plt.ylabel('EUR per ounce')
plt.grid()
plt.show()
Rolling Mean and Rolling Standard Deviation of Gold Prices
df3['rolling_mean_o'] = df3.oil.rolling(window=12).mean()
df3['rolling_std_o'] = df3.oil.rolling(window=12).std()
df3.plot(title='Rolling Mean and Rolling Standard Deviation of Crude Oil Prices', 
         figsize=(15,5))
plt.ylabel('EUR per barrel')
plt.show()
Rolling Mean and Rolling Standard Deviation of Crude Oil Prices
  • Plotting STD separately
df1['rolling_std_btc'].plot(title='Rolling standard deviation of Bitcoin prices', figsize=(10,3))
plt.show()
df2['rolling_std_g'].plot(title='Rolling standard deviation of Gold Prices', figsize=(10,3))
plt.show()
df3['rolling_std_o'].plot(title='Rolling standard deviation of Crude Oil Prices', figsize=(10,3))
plt.show()
Rolling standard deviation of BTC, Gold and Oil Daily Prices

Log Returns

  • Calculating log returns of BTC, Gold and Oil Daily Prices in EUR
df1['log_ret_btc'] = np.log(df1.btc/df1.btc.shift(1))
df2['log_ret_g'] = np.log(df2.gold/df2.gold.shift(1))
#Handling negative values
df3['trans_o'] = df3['oil'] + 1 - df3['oil'].min()
df3['log_ret_o'] = np.log(df3.trans_o/df3.trans_o.shift(1))

# Check the missing values of the series
print('Number of missing values of Bitcoin log returns:', df1.log_ret_btc.isna().sum())
print('Number of missing values of Gold log returns:', df2.log_ret_g.isna().sum())
print('Number of missing values of Crude oil log returns:', df3.log_ret_o.isna().sum())

# Plot 3 series of log returns
fig, ax = plt.subplots(3, 1, figsize=(10,9))
df1.log_ret_btc.plot(ax=ax[0])
df2.log_ret_g.plot(ax=ax[1])
df3.log_ret_o.plot(ax=ax[2])
fig.suptitle('Bitcoin (top), Gold (middle), and Crude Oil (bottom) Log Returns', fontsize=16)
Log returns of BTC, Gold and Oil Daily Prices in EUR
  • Plotting histograms of log returns
# Check the distribution of each series by histograms
fig, ax = plt.subplots(1, 3, figsize=(12,5))
sns.distplot(df1.log_ret_btc[1:], ax=ax[0]) 
sns.distplot(df2.log_ret_g[1:], ax=ax[1]) 
sns.distplot(df3.log_ret_o[1:], ax=ax[2])
fig.suptitle('Histograms of Log Returns of Bitcoin (left), Gold (middle), and Crude Oil (right)', fontsize=16)
plt.show()
Histograms of Log Returns of Bitcoin (left), Gold (middle), and Crude Oil (right)
  • Calculating the mean, skewness and kurtosis of these three distributions
print('- Bitcoin log returns:')
print('Mean:', df1.log_ret_btc.mean()) 
print('Skewness:', df1.log_ret_btc[1:].skew())
print('Kurtosis:', df1.log_ret_btc[1:].kurtosis()) 
print('\n')
print('- Gold log returns:')
print('Mean:', df2.log_ret_g.mean())
print('Skewness:', df2.log_ret_g[1:].skew())
print('Kurtosis:', df2.log_ret_g[1:].kurtosis())
print('\n')
print('- Crude oil log returns:', df3.log_ret_o.mean())
print('Skewness:', df3.log_ret_o[1:].skew())
print('Kurtosis:', df3.log_ret_o[1:].kurtosis())

- Bitcoin log returns:
Mean: 0.0005567042220352303
Skewness: -0.6250506415523658
Kurtosis: 6.352904867829025


- Gold log returns:
Mean: 0.0005128990945544017
Skewness: 0.057335219933718796
Kurtosis: 0.6080397539952869


- Crude oil log returns: 0.0012028385327495594
Skewness: -0.2601510498185974
Kurtosis: 10.065358812764817
  • The small positive mean return of each asset that the prices slightly increased over the period in general.
  • We can see that skewness(BTC)
  • 0The positive skewness of a distribution indicates that an investor may expect frequent small losses and a few large gains from the investment.
  • Kurtosis(Gold)=0.6<

ACF & PACF Analysis

  • Plotting ACF of Bitcoin (left), Gold (center), and Crude Oil (right) Prices
# ACF
fig, ax = plt.subplots(1, 3, figsize=(20,5))
# Omit the lag 0
sgt.plot_acf(df1.btc, lags=40, zero=False, ax=ax[0])
sgt.plot_acf(df2.gold, lags=40, zero=False, ax=ax[1])
sgt.plot_acf(df3.oil, lags=40, zero=False, ax=ax[2])
fig.suptitle('ACF of Bitcoin (left), Gold (center), and Crude Oil (right) Prices', fontsize=16)
plt.show()
ACF of Bitcoin (left), Gold (center), and Crude Oil (right) Prices
  • We can see a strong correlation of prices at most lags. Therefore, the time series are not random.
  • In other words, the these time series are non-stationary since all lags are significant (lie outside of blue areas) and gradually decrease.
  • Plotting ACF/PACF of log returns
fig, ax = plt.subplots(1, 2, figsize=(12,5))
# Omit the lag 0
sgt.plot_acf(df1.log_ret_btc[1:], lags=40, zero=False, ax=ax[0]) 
sgt.plot_pacf(df1.log_ret_btc[1:], lags=40, zero=False, method=('ols'), ax=ax[1])
fig.suptitle('ACF and PACF of Log Returns (Bitcoin)', fontsize=16)
plt.show()
ACF and PACF of Log Returns (Bitcoin)
  • The ACF/PACF of the BTC log returns shows that there is only one autocorrelation that is significantly non-zero at a lag of 0. Therefore, the time series is random.
#Gold Log Returns
fig, ax = plt.subplots(1, 2, figsize=(15,5))
# Omit the lag 0 
sgt.plot_acf(df2['log_ret_g'][1:], lags=30, zero=False, ax=ax[0])
sgt.plot_pacf(df2['log_ret_g'][1:], lags=30, zero=False, method=('ols'), ax=ax[1])
fig.suptitle('ACF and PACF of Gold Log Returns', fontsize=16)
plt.show()
ACF and PACF of Gold Log Returns
  • The ACF/PACF of the Gold log returns shows that there are a few autocorrelation values that are significantly non-zero. Therefore, the time series is not random.

ADF Test

  • Utilizing the Augmented Dickey–Fuller (ADF) test to determine the stationarity of daily prices
sts.adfuller(df1.btc)
(0.14442028431779896,
 0.9689662412819301,
 14,
 595,
 {'1%': -3.441388211993052,
  '5%': -2.8664097967491498,
  '10%': -2.569363480686392},
 10012.822783018151)

sts.adfuller(df2.gold)
(-0.6614074710243512,
 0.8564942854704949,
 7,
 580,
 {'1%': -3.4416749612171467,
  '5%': -2.8665360672844318,
  '10%': -2.5694307639714626},
 4866.972272767114)

sts.adfuller(df3.oil)
(-1.7466566758038544,
 0.4072478951328181,
 6,
 581,
 {'1%': -3.4416553818946145,
  '5%': -2.8665274458710064,
  '10%': -2.5694261699959413},
 2524.633258481832)
  • Running the ADF test to determine the stationarity of log returns
sts.adfuller(df1.log_ret_btc[1:])
(-25.242828636652007,
 0.0,
 0,
 608,
 {'1%': -3.4411511898545206,
  '5%': -2.866305414688104,
  '10%': -2.569307861950312},
 -2248.7536541086974)

sts.adfuller(df2.log_ret_g[1:])
(-9.20998879011992,
 1.8991624861248938e-15,
 6,
 580,
 {'1%': -3.4416749612171467,
  '5%': -2.8665360672844318,
  '10%': -2.5694307639714626},
 -3641.2449649515574)

sts.adfuller(df3.log_ret_o[1:])
(-6.240678969886013,
 4.70563427292106e-08,
 17,
 569,
 {'1%': -3.4418948967017475,
  '5%': -2.8666329082535222,
  '10%': -2.569482366838501},
 -230.3750311555698)
  • This test shows that p-value << 0.05 for all three log returns. A very low p-value of ADF tells that the null hypothesis of the test is very unlikely to be correct. In the ADF test, it would mean the tested series is stationary.

SARIMAX Summary

  • Fitting the possible ARIMA models for BTC log returns
model_btc_arma1= sm.tsa.arima.ARIMA(df1.log_ret_btc[1:], order = (1,0,1))
model_btc_arma2= sm.tsa.arima.ARIMA(df1.log_ret_btc[1:], order = (2,0,2))
model_btc_arma3= sm.tsa.arima.ARIMA(df1.log_ret_btc[1:], order = (2,0,1))

results_btc_arma1=model_btc_arma1.fit()
results_btc_arma2=model_btc_arma2.fit()
results_btc_arma3=model_btc_arma3.fit()

#BTC arma1 
print(results_btc_arma1.summary())

SARIMAX Results                                
==============================================================================
Dep. Variable:            log_ret_btc   No. Observations:                  609
Model:                 ARIMA(1, 0, 1)   Log Likelihood                1166.391
Date:                Fri, 03 May 2024   AIC                          -2324.781
Time:                        16:18:35   BIC                          -2307.134
Sample:                    01-04-2022   HQIC                         -2317.916
                         - 05-03-2024                                         
Covariance Type:                  opg                                         
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
const          0.0006      0.001      0.377      0.706      -0.002       0.003
ar.L1         -0.0129      1.262     -0.010      0.992      -2.486       2.461
ma.L1         -0.0145      1.252     -0.012      0.991      -2.469       2.440
sigma2         0.0013   3.73e-05     34.091      0.000       0.001       0.001
===================================================================================
Ljung-Box (L1) (Q):                   0.00   Jarque-Bera (JB):              1075.83
Prob(Q):                              0.97   Prob(JB):                         0.00
Heteroskedasticity (H):               0.54   Skew:                            -0.64
Prob(H) (two-sided):                  0.00   Kurtosis:                         9.38
===================================================================================

#BTC arma2
print(results_btc_arma2.summary())

SARIMAX Results                                
==============================================================================
Dep. Variable:            log_ret_btc   No. Observations:                  609
Model:                 ARIMA(2, 0, 2)   Log Likelihood                1166.580
Date:                Fri, 03 May 2024   AIC                          -2321.159
Time:                        16:18:35   BIC                          -2294.688
Sample:                    01-04-2022   HQIC                         -2310.862
                         - 05-03-2024                                         
Covariance Type:                  opg                                         
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
const          0.0006      0.001      0.374      0.708      -0.002       0.003
ar.L1         -0.2122      2.739     -0.077      0.938      -5.580       5.156
ar.L2         -0.3698      0.893     -0.414      0.679      -2.120       1.380
ma.L1          0.1788      2.741      0.065      0.948      -5.193       5.550
ma.L2          0.3623      0.900      0.402      0.687      -1.402       2.127
sigma2         0.0013   3.72e-05     34.099      0.000       0.001       0.001
===================================================================================
Ljung-Box (L1) (Q):                   0.03   Jarque-Bera (JB):              1081.50
Prob(Q):                              0.85   Prob(JB):                         0.00
Heteroskedasticity (H):               0.54   Skew:                            -0.64
Prob(H) (two-sided):                  0.00   Kurtosis:                         9.40
===================================================================================

#BTC arma3

print(results_btc_arma3.summary())

  SARIMAX Results                                
==============================================================================
Dep. Variable:            log_ret_btc   No. Observations:                  609
Model:                 ARIMA(2, 0, 1)   Log Likelihood                1166.399
Date:                Fri, 03 May 2024   AIC                          -2322.798
Time:                        16:18:36   BIC                          -2300.739
Sample:                    01-04-2022   HQIC                         -2314.216
                         - 05-03-2024                                         
Covariance Type:                  opg                                         
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
const          0.0006      0.001      0.371      0.711      -0.002       0.003
ar.L1         -0.0129      7.081     -0.002      0.999     -13.892      13.866
ar.L2          0.0050      0.189      0.026      0.979      -0.365       0.375
ma.L1         -0.0130      7.078     -0.002      0.999     -13.886      13.860
sigma2         0.0013   3.73e-05     34.062      0.000       0.001       0.001
===================================================================================
Ljung-Box (L1) (Q):                   0.00   Jarque-Bera (JB):              1072.67
Prob(Q):                              1.00   Prob(JB):                         0.00
Heteroskedasticity (H):               0.54   Skew:                            -0.63
Prob(H) (two-sided):                  0.00   Kurtosis:                         9.38
===================================================================================
  • Printing AIC and BIC of these three models
print("ARMA(1,1)", '- AIC:', results_btc_arma1.aic, ', BIC:', results_btc_arma1.bic)
print("ARMA(2,2)", '- AIC:', results_btc_arma2.aic, ', BIC:', results_btc_arma2.bic)
print("ARMA(2,1)", '- AIC:', results_btc_arma3.aic, ', BIC:', results_btc_arma3.bic)
ARMA(1,1) - AIC: -2324.7814305521733 , BIC: -2307.1341574813337
ARMA(2,2) - AIC: -2321.159332278072 , BIC: -2294.6884226718125
ARMA(2,1) - AIC: -2322.797845957811 , BIC: -2300.7387546192617
  • Selecting the final model ARMA(1,1) with the lowest AIC
model_btc_arima = model_btc_arma1
results_btc_arima = model_btc_arima.fit()

Scipy Q-Q Plot of BTC Residuals

  • Creating the BTC residuals for ARMA(1,1)
# create residual series
df1['res_btc'] = results_btc_arima.resid
  • Examining the BTC residuals, ACF, histogram/density and Q-Q plot
fig, ax = plt.subplots(1, 2, figsize=(15,6))
# ACF of the residuals
sgt.plot_acf(df1.res_btc[1:], lags=40, zero=False, ax=ax[0])
# Plot the residuals
df1.res_btc.plot(title=" Residuals (Bitcoin)", ax=ax[1])
plt.show()

fig, ax = plt.subplots(1, 2, figsize=(15,6))
# Histogram of the estimated residuals
sns.distplot(df1.res_btc[1:], ax=ax[0]) 
# QQ-plot of the estimated residuals
scipy.stats.probplot(df1.res_btc[1:], plot=pylab)
pylab.show()
ACF, residuals, density and Q-Q plot
# Check the mean of the BTC residuals
print('Mean of the residuals (Bitcoin):', df1.res_btc[1:].mean())

Mean of the residuals (Bitcoin): 1.5444960244847711e-06

Shapiro-Wilks Test

  • Running the Shapiro-Wilk Test for Normality of the BTC residuals
stats.shapiro(df1.res_btc[1:])
ShapiroResult(statistic=0.9235081076622009, pvalue=4.887759285250107e-17)
  • Since the test is significant (p < . 05), the distribution of BTC residuals is significantly different from a normal distribution.

ARCH Test

  • Applying the ARCH test of conditional volatility to the squared BTC residuals
# ARCH test on the squared residuals
het_arch(df1.res_btc[1:]**2, ddof=2)
(0.5846557241264584,
 0.9999860444470244,
 0.05764063065878104,
 0.9999865614735265)
  • The test indicates that pvalue=1.0 is larger than the significant level of 0.05. We do not have enough evidence to reject the null hypothesis at 5% and conclude no presence of ARCH effects.

Ljung-Box Test

  • Applying the Ljung-Box test to the BTC residuals after fitting the ARMA(1, 1) model
acorr_ljungbox(df1.res_btc[1:], lags=[30], return_df=True) 

    lb_stat lb_pvalue
30 19.863358 0.920051
  • A significant p-value in this test rejects the null hypothesis that the time series isn’t autocorrelated. The alternate hypothesis, Ha, is just that the model does show a lack of fit.

Annualized Returns vs Volatility

  • Creating a copy of the original dataset, dropping missing values and deleting unnecessary columns
# Create a copy of original data 
df = data.copy()
df.isna().sum()
Ticker
BTC-USD       0
CL=F        264
EURUSD=X    242
GC=F        264
btc         242
gold        264
oil         264
dtype: int64

# Delete the surplus columns
del df['BTC-USD'], df['CL=F'], df['GC=F'], df['EURUSD=X']
df.dropna(inplace=True)
  • Calculating log returns and removing the first missing value
# Create log returns
rets = np.log(df/df.shift(1))
rets.head()

Ticker btc gold oil
Date   
2022-01-03  NaN      NaN       NaN
2022-01-04 -0.005858 0.014362 0.018171
2022-01-05 -0.050418 0.007475 0.012757
2022-01-06 -0.012054 -0.022516 0.017826
2022-01-07 -0.036378 0.006099 -0.005603

# Remove the first missing values
rets.dropna(inplace=True)

rets.isna().sum()

Ticker
btc     0
gold    0
oil     0
dtype: int64

# Rename with actual tickers for convenience
rets.rename(columns={"btc": "BTC-EUR", "gold": "GC=F", "oil": "CL=F"}, inplace=True)

# Define our financial instruments 
symbols = ['BTC-EUR', 'GC=F', 'CL=F']
noa = len(symbols)
  • Calculating the annualized returns/volatility
n=1
mean = rets.mean()*n*252
table1 = pd.DataFrame()
table1["Annualized Returns"] = mean

std = rets.std()*math.sqrt(n*252)
table2 = pd.DataFrame()
table2["Annualized Volatility"] = std

table = pd.concat([table1, table2], axis=1, join='inner')
table

Annualized Returns Annualized Volatility
Ticker  
BTC-EUR 0.145547 0.576556
GC=F    0.129251 0.158081
CL=F    0.037514 0.410363
  • This output should be multiplied by the total number of years n>1 to get the total Returns vs Volatility.
  • Comparing the Risk and Return of these three assets
plt.figure(figsize=(8,5))
sns.scatterplot(data=table, x="Annualized Volatility", y="Annualized Returns", legend="auto",s=100)
plt.title('Risk and Return of Three Assets') 
plt.text(x=table.iloc[0]['Annualized Volatility'],y=table.iloc[0]['Annualized Returns'],s="BTC-EUR")
plt.text(x=table.iloc[1]['Annualized Volatility'],y=table.iloc[1]['Annualized Returns'],s="GC=F")
plt.text(x=table.iloc[2]['Annualized Volatility'],y=table.iloc[2]['Annualized Returns'],s="CL=F")
plt.grid()
plt.show()
Risk and Return of our three assets
  • This plot shows that BTC gives the highest return, but also highest risk, the Gold asset comes in second with the lowest risk and the Oil asset give the lowest return with the highest volatility.

Covariance vs Correlation Coefficient

  • Calculating the covariance and correlation coefficient of annualized returns
n=1
rets.cov()*n* 252

Ticker   BTC-EUR  GC=F    CL=F
Ticker   
BTC-EUR 0.332417 0.011029 0.012148
GC=F    0.011029 0.024990 0.020253
CL=F    0.012148 0.020253 0.168398

rets.corr()

Ticker  BTC-EUR  GC=F     CL=F
Ticker   
BTC-EUR 1.000000 0.121011 0.051343
GC=F    0.121011 1.000000 0.312208
CL=F    0.051343 0.312208 1.000000

  • It is clear that the assets have weak correlation.
  • cov(Gold)<

Monte Carlo Simulation of Expected Returns vs Volatility

  • Calculating and plotting the expected returns vs volatility for 6000 random portfolios
n=1
weights = np.random.random(noa)
weights /= np.sum(weights)

def port_ret(weights):
    return np.sum(rets.mean() * weights) * n* 252

prets = []
pvols = []
for p in range (6000):
    weights = np.random.random(noa)
    weights /= np.sum(weights)
    prets.append(port_ret(weights))
    pvols.append(port_vol(weights))
prets = np.array(prets)
pvols = np.array(pvols)

# Visualization of the Monte Carlo Simulation.
plt.figure(figsize=(10, 6))
plt.scatter(pvols, prets, c=prets / pvols, marker='o', cmap='coolwarm', edgecolors='green')
plt.xlabel('Expected Volatility')
plt.ylabel('Expected Return')
plt.colorbar(label='Sharpe Ratio');
Monte Carlo Simulation of random portfolios

Scipy SLSQP Optimization of Portfolio Weights

  • Running the Scipy SLSQP optimization of portfolio weights with constraints and bounds. The Sharpe ratio’s negative value is minimized in order to derive at the maximum value and the optimal portfolio composition. The constraint is that all parameters (weights) add up to 1. Weights are bound to be between 0 and 1.
def min_func_sharpe(weights):
    return -(port_ret(weights) - 0.0005) / port_vol(weights)

cons = ({'type': 'eq', 'fun': lambda x:  np.sum(x) - 1})

bnds = tuple((0, 1) for x in range(noa))
eweights = np.array(noa * [1. / noa,])
eweights
array([0.33333333, 0.33333333, 0.33333333])

#The maximum Sharpe ratio portfolio

opts = sco.minimize(min_func_sharpe, eweights,
                             method='SLSQP', bounds=bnds,
                             constraints=cons)

opts

message: Optimization terminated successfully
 success: True
  status: 0
     fun: -0.8289173529075161
       x: [ 5.057e-02  9.494e-01  1.908e-17]
     nit: 5
     jac: [-5.614e-03 -3.070e-03  4.332e-01]
    nfev: 20
    njev: 5


print(f"Optimal Portfolio Weights: {opts['x'].round(3)}")
print(f"Optimal Portfolio Return: {port_ret(opts['x']).round(3)}")
print(f"Optimal Portfolio Volatility: {port_vol(opts['x']).round(3)}")
print('The maximum Sharpe ratio:', port_ret(opts['x']) / port_vol(opts['x']))

Optimal Portfolio Weights: [0.051 0.949 0.   ]
Optimal Portfolio Return: 0.13
Optimal Portfolio Volatility: 0.156
The maximum Sharpe ratio: 0.8321159608481458

#Minimum Variance Portfolio

optv = sco.minimize(port_vol, eweights, method='SLSQP', bounds=bnds, constraints=cons)

optv

message: Optimization terminated successfully
 success: True
  status: 0
     fun: 0.1558088348592799
       x: [ 4.109e-02  9.294e-01  2.948e-02]
     nit: 6
     jac: [ 1.557e-01  1.558e-01  1.559e-01]
    nfev: 24
    njev: 6

mvp_weights = optv.x

print(f"MVP Weights: {optv['x'].round(3)}")
print(f"MVP Return: {port_ret(mvp_weights).round(2)}")
print(f"MVP Volatility: {port_vol(mvp_weights).round(2)}")
print('Sharpe ratio of MVP:', port_ret(optv['x'])/port_vol(optv['x']))

MVP Weights: [0.041 0.929 0.029]
MVP Return: 0.13
MVP Volatility: 0.16
Sharpe ratio of MVP: 0.8164869035640154

cons = ({'type': 'eq', 'fun': lambda x:  port_ret(x) - tret}, 
        {'type': 'eq', 'fun': lambda x:  np.sum(x) - 1})

trets = np.linspace(0.12, 0.56, 50)
tvols = []
for tret in trets:
    res = sco.minimize(port_vol, eweights, method='SLSQP', bounds=bnds, constraints=cons)
    tvols.append(res['fun'])
tvols = np.array(tvols)

Final Comparison of Efficient Frontier & Min Variance Portfolios

  • Comparing the Monte Carlo efficient frontier, tangency and minimum variance portfolios
plt.figure(figsize=(12, 9))
plt.scatter(pvols, prets, c=prets / pvols, marker='.', alpha=0.6, cmap='coolwarm')
plt.plot(tvols, trets, 'b', lw=3.0)
plt.plot(port_vol(opts['x']), port_ret(opts['x']), 'y*', markersize=15.0, label='Tangency porfolio')
plt.plot(port_vol(optv['x']), port_ret(optv['x']),'r*', markersize=15.0, label='Minimum Variance Portfolio')

#BTC-EUR 
aly=0.139514 
alx=0.577075
plt.plot(alx, aly,'g^', markersize=15.0, label='BTC-EUR')

#GC-F
aly=0.134493 
alx=0.157786
plt.plot(alx, aly,'b+', markersize=35.0, label='GC-F')

#CL-F
aly=0.057542 
alx=0.410822
plt.plot(alx, aly,'cs', markersize=20.0, label='CL-F')

plt.legend()
plt.xlabel('Expected Volatility')
plt.ylabel('Expected Return')
plt.grid()
plt.ylim([0, 0.2])
plt.colorbar(label='Sharpe Ratio')
Final Comparison: efficient frontier, tangency and minimum variance portfolios.
  • The solid line represents the optimal portfolios for a particular target return, while the dots represent random portfolios. Furthermore, the figure depicts two larger stars, one for the minimum volatility/variance portfolio (red star symbol) and the other for the portfolio with the maximum Sharpe ratio (yellow star symbol).

Conclusions

  • The portfolio Bitcoin, Gold, and Oil is highly volatile. Almost all the budget is spent on the Gold instrument.
  • The maximum Sharpe ratio of 0.83 is not acceptable. Usually, any Sharpe ratio greater than 1.0 is considered acceptable to good by investors. This means we are not being compensated very well for the risk we have taken on.
  • The proposed portfolio has diversification capabilities that do not complement each other.
  • However, gold and stocks are driven by separate factors. If an investment portfolio is very heavily inclined toward one asset class or industry, then buying gold might be something to consider.

Sources

Explore More

Tutorials for Beginners

Contacts

Infographics

5-step roadmap towards min(risk/Return) ratio (image template via Canva)
Blue-Chip Risk-Aware Trading Strategy (image template via Canva)

Embed X Socials

Python
Algorithmic Trading
Portfolio Management
Gold
Btc
Recommended from ReadMedium