تحلیل سبد سرمایه گذاری ارزهای دیجیتال در پایتون

 

تبدیل شدن به یک میلیونر کریپتو چنان که به نظر می رسد، ساده نیست. اما با به کار بردن استراتژی سرمایه گذاری مناسب می‌توان حاشیه‌ی سود زیادی در مدت زمان کمتری کسب کرد. در این پست قصد داریم یک استراتژی سبد سرمایه گذاری را برای ارزهای دیجیتال Cryptocurrency با بکارگیری ابزارهای علوم داده پلتفرم پایتون Python و مدل پایه‌ای پورتفولیو مارکویتز Markowitz Portfolio Optimzation پیشنهاد دهیم.

هشدار !
این پست صرفا جنبه آموزشی داشته و مسئولیت هرگونه استفاده جهت سرمایه‌گذاری متوجه خود فرد می‌باشد. تمامی اسکریپت‌ها برای پایتون نسخه ۳ به بالاتر قابل اجرا هستند.

برای شروع به کار کتابخانه‌های زیر بایستی بر روی پلتفرم پایتون نصب شده باشند.

import os
import numpy as np
from numpy import linalg as la
import pandas as pd
import pickle
from datetime import datetime
import plotly.offline as py
import plotly.graph_objs as go
py.init_notebook_mode(connected=True)
import matplotlib.pyplot as plt
from gurobipy import GRB,Model

 

برای نصب کتابخانه gurobi توصیه می‌شود از بسته‌ی نرم‌افزاری Anaconda از طریق این لینک اقدام نمایید.

کتابخانه pandas از فرمت‌های داده‌ای json، csv و xml پشتیبانی می‌کند. از دو تابع زیر جهت واکشی اطلاعات از json می‌توان بهره گرفت.

def get_json_data(json_url, cache_path):
'''Download and cache JSON data, return as a dataframe.'''
try:
f = open(cache_path, 'rb')
df = pickle.load(f)
print('Loaded {} from cache'.format(json_url))
except (OSError, IOError) as e:
print('Downloading {}'.format(json_url))
df = pd.read_json(json_url)
df.to_pickle(cache_path)
print('Cached {} at {}'.format(json_url, cache_path))
return df
def get_crypto_data(poloniex_pair):
'''Retrieve cryptocurrency data from poloniex'''
json_url = base_polo_url.format(poloniex_pair, start_date.timestamp(), end_date.timestamp(), period)
data_df = get_json_data(json_url, poloniex_pair)
data_df = data_df.set_index('date')
return data_df

 

حال ۹ ارز دیجیتال متفاوت شامل بیت کوین BIT، اتریوم ETH، ریپل XRP، دش DASH، مونرو XMR، لایت کوین LTC، زی کش ZEC، ای او اس EOS و استلار STR را جهت واکشی اطلاعات از صرافی poloniex می‌کنیم. وب سرویس عمومی این صرافی به آدرس https://docs.poloniex.com انجام این عملیات را به صورت رایگان در اختیار عموم قرار می‌دهد. توجه شود که از جفت‌های USDT که نرخی مشابه نرخ ارز دلار را دارد، استفاده شده است.

 

altcoins = ['BTC','ETH','XRP','DASH','XMR','LTC','ZEC','EOS','STR']
altcoin_data = {}
for altcoin in altcoins:
coinpair = 'USDT_{}'.format(altcoin)
crypto_price_df = get_crypto_data(coinpair)
altcoin_data[altcoin] = crypto_price_df
base_polo_url = 'https://poloniex.com/public?command=returnChartData&currencyPair={}&start={}&end={}&period={}'
start_date = datetime.strptime('2018-01-01', '%Y-%m-%d') # get data from the start of 2015
end_date = datetime.now() # up until today
period = 86400 # pull daily data (86,400 seconds per day)

 

از دستورات زیر جهت ادغام قیمت‌های ارزهای دیجیتال بر مبنای “قیمت آخر روز” close استفاده می‌کنیم.

def merge_dfs_on_column(dataframes, labels, col):
'''Merge a single column of each dataframe into a new combined dataframe'''
series_dict = {}
for index in range(len(dataframes)):
series_dict[labels[index]] = dataframes[index][col]
return pd.DataFrame(series_dict)
df = merge_dfs_on_column(list(altcoin_data.values()), list(altcoin_data.keys()), 'close')
dateRange = pd.date_range('2018-6-1', periods=365, freq='D')
df = df.loc[dateRange]
fig= plt.figure(figsize=(10,6))
plt.plot(df)
plt.legend(df)

 

متاسفانه نوسانات قیمتی بیت‌کوین مانع از وضوح نوسانات قیمتی سایر ارزها در نمودار شده است. بنابراین یکبار دیگر این روند را بدون در نظر گرفتن بیت‌کوین در نظر می‌گیریم. در نمودار زیر واضح است که روند افزایشی و کاهشی ارزها تقریبا مشابه بوده و انتظار می‌رود که از ماتریس کواریانس جذابی برخوردار نباشند.

 

خلاصه‌ای از انتهای dataFrame قیمت ارزهای مختلف با دستور زیر قابل انتشار است:

df.tail()

 

 

همچنین آمار توصیفی این dataFrame با دستور زیر فراخوانی می‌شود:

df.describe()

 

 

 

همانظور که پیش‌بینی می‌شد ماتریس کواریانس برای تخمین ریسک مدل پورتفولیو جذاب نیست. علی‌رغم این چالش، تکمیل مدل را ادامه می‌دهیم.

trace = go.Heatmap(z=df.corr().values.tolist(), x=altcoins, y=altcoins)
data=[trace]
py.iplot(data)

 

 

این احتمال وجود دارد که ماتریس کواریانس نیمه معین مثبت semi-definite positive نبوده و تحدب تابع هدف را به مخاطره بیاندازد. نزدیک‌ترین ماتریس معین مثبت به ماتریس کوواریانس قیمت‌ها به کمک تابع زیر بازیابی می‌شود:

def nearestPD(A):
"""Find the nearest positive-definite matrix to input
N.J. Higham, "Computing a nearest symmetric positive semidefinite
matrix" (1988): https://doi.org/10.1016/0024-3795(88)90223-6
"""
B = (A + A.T) / 2
_, s, V = la.svd(B)
H = np.dot(V.T, np.dot(np.diag(s), V))
A2 = (B + H) / 2
A3 = (A2 + A2.T) / 2
if isPD(A3):
return A3
spacing = np.spacing(la.norm(A))
I = np.eye(A.shape[0])
k = 1
while not isPD(A3):
mineig = np.min(np.real(la.eigvals(A3)))
A3 += I * (-mineig * k**2 + spacing)
k += 1
return A3
def isPD(B):
"""Returns true when input is positive-definite, via Cholesky"""
try:
_ = la.cholesky(B)
return True
except la.LinAlgError:
return False
if __name__ == '__main__':
import numpy as np
for i in range(10):
for j in range(2, 100):
A = np.random.randn(j, j)
B = nearestPD(A)
assert(isPD(B))
print('unit test passed!')

 

میانگین بازدهی و کوواریانس بین قیمت‌ها بر اساس مغیرهای لیستی list به صورت زیر بازنویسی می‌شود:

Mu = df.mean().values.tolist()
Corr = nearestPD(df.cov()).values.tolist()

 

در نهایت مدل پورتفولیو زیر به کمک توابع و کتابخانه‌های بسته بهینه‌سازی گروبی Gurobi ارائه می‌شود:

 

$$\begin{array}{ll} \mbox{minimize} & x^T \Sigma x\\
\mbox{subject to} & {\bf 1}^T x = 1,\\
& \mu^T x \geq R, \\
& x \in \mathbb{R}_+
\end{array}$$

 

m = Model("portfolio")
m.setParam('OutputFlag', 0)
x = [m.addVar(name=symb) for symb in altcoins]
m.setObjective(sum(Corr[i][j]*x[i]*x[j] for i in range(len(altcoins)) for j in range(len(altcoins))), GRB.MINIMIZE)
m.addConstr(sum(x[i] for i in range(len(altcoins))) ==1)

 

۴۵ سبد ارائه می‌شود که نمودار پارتو سبد‌ها با افزایش میانگین بازدهی هر سبد ترسیم می‌گردد. یکی از تکنیک‌های بکار گرفته شده در ترسیم نمودار پارتو تابع getConstrByName جهت حذف محدودیت در هر تکرار بهینه‌سازی است.

# Compute trade-off curve.
MinTarget = [i for i in range(500,5000,100)]
risk_data = np.zeros(len(MinTarget))
ret_data = np.zeros(len(MinTarget))
for j in range(len(MinTarget)):
m.addConstr(sum(x[i]*Mu[i] for i in range(len(altcoins))) >= MinTarget[j],'BudgetConstr')
m.optimize()
Invest = m.getAttr('x',x)
Risk = m.objVal
m.remove(m.getConstrByName('BudgetConstr'))
m.update()
risk_data[j] = np.sqrt(Risk)
ret_data[j] = df.mean().T.dot(Invest)
# or --> sum(Mu[i]*Invest[i] for i in range(len(altcoins)))
# Plot long only trade-off curve.
trace = go.Scatter(
x = risk_data,
y = ret_data,
mode = 'lines+markers+text',
text=[i+1 for i in range(len(MinTarget))],
textposition='bottom center'
)
data = [trace]
layout = go.Layout(
title='Risk Analysis of Portfolio',
xaxis=dict(
title='STD(Risk)'
),
yaxis=dict(
title='Return'
)
)
fig = go.Figure(data=data, layout=layout)
py.iplot(fig)

 

و در نهایت ۴۵ پورتفولیوی بر اساس میزان بازدهی متفاوت در ادامه پیشنهاد شده است. در تمامی این سبدها ارزهای بیت کوین و ریپل با سهم‌های متفاوت پیشنهاد می‌گردد!

def stacked_bar(data, series_labels, category_labels=None,
show_values=False, value_format="{}", y_label=None,
grid=True, reverse=False):
"""Plots a stacked bar chart with the data and labels provided.
Keyword arguments:
data -- 2-dimensional numpy array or nested list
containing data for each series in rows
series_labels -- list of series labels (these appear in
the legend)
category_labels -- list of category labels (these appear
on the x-axis)
show_values -- If True then numeric value labels will
be shown on each bar
value_format -- Format string for numeric value labels
(default is "{}")
y_label -- Label for y-axis (str)
grid -- If True display grid
reverse -- If True reverse the order that the
series are displayed (left-to-right
or right-to-left)
"""
ny = len(data[0])
ind = list(range(ny))
axes = []
cum_size = np.zeros(ny)
data = np.array(data)
if reverse:
data = np.flip(data, axis=1)
category_labels = reversed(category_labels)
for i, row_data in enumerate(data):
axes.append(plt.bar(ind, row_data, bottom=cum_size,
label=series_labels[i]))
cum_size += row_data
if category_labels:
plt.xticks(ind, category_labels)
if y_label:
plt.ylabel(y_label)
plt.legend()
if grid:
plt.grid()
plt.figure(figsize=(10, 6))
series_labels = altcoins
data = Invest_mat.T
# category_labels = [i+1 for i in range(len(MinTarget))]
stacked_bar(
data,
series_labels,
# category_labels=category_labels,
show_values=True,
value_format="{:.1f}",
y_label="Quantity (units)"
)
# plt.savefig('bar.png')
plt.show()

 

در صورتی که میزان بیت‌کوین و ریپل را در سبدها محدود کنیم گزینه‌ی بعدی استلار خواهد بود.

m.addConstr(x[0]+x[2]<=.9)

 

 

کد نوت‌بوک Jupyter به صورت یکجا در لینک زیر بارگذاری شده است.

این محتوا برای اعضا قابل مشاهده است. لطفا وارد شوید

 

2 Comments
Inline Feedbacks
View all comments
مرتضی
آگوست 8, 2019 5:50 ب.ظ

جالب بود! آیا ماینرها هم همین شکلی عمل میکنند؟ ممنون