PainLove 2018-08-12
今天我们将深入投资组合优化领域。我们将使用Python生态系统周围的高级工具、金融风险建模和一点机器学习来构建一个加密投资组合优化器。
虽然不是绝对必要,但我建议看一下Jupyter Notebooks。它通过结合文本编辑器,交互式控制台和可视化仪表板的功能,将Read-Eval-Print循环提升到一个全新的水平。
首先,我们需要导入以下库:
import json
import requests
import pandas as pd
import numpy as np
import tensorflow as tf
# import matplotlib.pyplot as plt
json并且requests不言自明。pandas对数据系列和数据分析非常有用。numpy主要用它来处理矩阵。tensorflow将用于优化我们产品组合的权重,matplotlib让我们可以在Notebook上显示数据。
要开始处理数据,我们需要从API端点检索它。在我们的例子中,我们将使用CryptoCompare。Python实现的代码如下:
# GLOBAL
coins = ["BTC", "ETH", "LTC", "XLM", "IOT", "XRP", "USDT"]
days_ago_to_fetch = 400 # see also filter_history_by_date()
coin_history = {}
hist_length = 0
average_returns = {}
cumulative_returns = {}
def fetch_all():
for coin in coins:
coin_history[coin] = fetch_history(coin)
def fetch_history(coin):
endpoint_url = "https://min-api.cryptocompare.com/data/histoday?fsym={}&tsym=USD&limit={:d}".format(coin, days_ago_to_fetch)
res = requests.get(endpoint_url)
hist = pd.DataFrame(json.loads(res.content)['Data'])
hist = index_history(hist)
hist = filter_history_by_date(hist)
return hist
def index_history(hist):
# index by date so we can easily filter by a given timeframe
hist = hist.set_index('time')
hist.index = pd.to_datetime(hist.index, unit='s')
return hist
def filter_history_by_date(hist):
result = hist[hist.index.year >= 2017]
# result = result[result.index.day == 1] # every first of month, etc.
return result
fetch_all()
hist_length = len(coin_history[coins[0]])
请注意,除了提取和解析JSON数据之外,我们还使用Pandas将time列转换为Python日期和索引。这允许我们按任何布尔条件过滤行并非常容易地处理日期条件。
在上面的代码中,我们coin_history使用Pandas数据框填充了字典。因此,让我们添加一个新的代码块,输入类似的表达式coin_history['BTC']并按Enter键:
这里是BTC历史的excerp,很好地打印到我们的Web控制台。
接下来,我们需要添加两列,它们的值来自当前列:(daily)return和excess return。
资产的return是价格变化与初始价格之间的比率。
一个时期内的excess return是平均收益和实际收益的差值。
在正常情况下,我们需要为每一行编写一个循环并计算值。然而,panda允许使用下面这样的一行程序对整个数组执行操作:
# Calculate returns and excess returns
def add_all_returns():
for coin in coins:
hist = coin_history[coin]
hist['return'] = (hist['close'] - hist['open']) / hist['open']
average = hist["return"].mean()
average_returns[coin] = average
cumulative_returns[coin] = (hist["return"] + 1).prod() - 1
hist['excess_return'] = hist['return'] - average
coin_history[coin] = hist
add_all_returns()
# display data
cumulative_returns
这里发生了几件事:
现在,average_returns并cumulative_returns包含以下值:
现在是时候将excess returns合并到n × k Excess return矩阵X中,其中n是观察数量(天,周,月......),k是投资组合中的资产数量。
Excess Return Matrix
第一行创建一个n×k矩阵和循环将相应的值分配给每个coin。
然而,如果我们打印矩阵的值,我们将得到numpy数组的原始内容,这不是很有意义。
让我们将矩阵包装成Pandas DataFrame,设置适当的索引(日期)和列标题(coin名称):
更容易评估,不是吗?
有许多方法可用于优化投资组合。在本文中,我们将分析单个资产的方差和协方差,以最大限度地降低全局风险。
为此,我们将使用我们的Excess Return矩阵从中计算方差 - 协方差矩阵 Σ。
计算它就像以下一样简单:
#方差协方差矩阵
product_matrix = np.matmul(excess_matrix.transpose(),excess_matrix)
var_covar_matrix = product_matrix / hist_length
正确显示,看起来像:
在这个矩阵中:
在我们进入实际的投资组合优化之前,我们的下一个目标是相关矩阵,其中每个项目的定义如下:
资产X和Y之间的相关性是它们的协方差除以它们的标准偏差的乘积。
我们已经存储了cov(X,Y),var_covar_matrix因此我们需要一个k × k矩阵,其中包含每个标准差的乘积。
让我们计算各个标准偏差:
# Standard Deviation
std_deviations = np.zeros((len(coins), 1))
for idx, coin in enumerate(coins):
std_deviations[idx][0] = np.std(coin_history[coin]['return'])
利用NumPy,以下是值:
要生成带有标准偏差乘积的矩阵,我们将其乘以其转置。
# Std Deviation products matrix
sdev_product_matrix = np.matmul(std_deviations, std_deviations.transpose())
这是:
所以现在,我们可以最终计算相关矩阵,正如我们之前定义的那样。
#相关矩阵
correlation_matrix = var_covar_matrix / sdev_product_matrix
它看起来怎样?
在相关矩阵中:
鉴于平均回报和我们资产的方差,现在是时候决定每个资产的分配金额了。
在这一点上,我们希望找到一种投资组合,最大限度地减少投资组合的全局方差。
权重数组是我们希望从我们的投资组合优化器获得的输出。每个资产的权重范围为0到1,总和必须为1。
给定权重数组,我们可以将加权标准差定义为:
因此,我们投资组合的全局方差现在可以定义为:
其中W是具有加权标准偏差的1× k矩阵,C是上述相关矩阵,结果是具有全局投资组合方差的1×1矩阵。
这是我们想要最小化的价值,但我们怎么做呢?
我们可以定义计算给定权重数组的全局方差的函数,探索所有可能的候选者并对它们进行评级。
然而,找到具有k个变量的方程的绝对最小值是NP问题。如果我们试图评估每个可能的解决方案,计算量将随k呈指数增长。
因此,我们手中最好的选择是使用机器学习为我们探索搜索空间的不同子集,并让它探索有潜力比同类分支表现更好的分支变体。
这就是Tensorflow发挥作用的地方。TensorFlow是最初由Google开发的开源机器学习框架。
与大多数库的主要区别在于,它不是直接执行操作,而是提供一组方法来描述它们,以便可以对它们执行训练和优化。操作,比如tf.multiply(x, y)将不会返回实际的结果,而是一个operation 可以run in a session和评估,最终。
会话由相互关联的操作和变量组成,它们可以生成CPU,GPU和专用TPU单元。这允许我们描述对变量的操作(而不是数值),并让框架最小化我们的目标值。
让我们用Python编写第一个解决问题的方法:
# Optimize weights to minimize volatility
def minimize_volatility():
# Define the model
# Portfolio Volatility = Sqrt (Transpose (Wt.SD) * Correlation Matrix * Wt. SD)
coin_weights = tf.Variable(np.full((len(coins), 1), 1.0 / len(coins))) # our variables
weighted_std_devs = tf.multiply(coin_weights, std_deviations)
product_1 = tf.transpose(weighted_std_devs)
product_2 = tf.matmul(product_1, correlation_matrix)
portfolio_variance = tf.matmul(product_2, weighted_std_devs)
portfolio_volatility = tf.sqrt(tf.reduce_sum(portfolio_variance))
# Run
learn_rate = 0.01
steps = 5000
init = tf.global_variables_initializer()
# Training using Gradient Descent to minimize variance
train_step = tf.train.GradientDescentOptimizer(learn_rate).minimize(portfolio_volatility)
with tf.Session() as sess:
sess.run(init)
for i in range(steps):
sess.run(train_step)
if i % 1000 == 0 :
print("[round {:d}]".format(i))
print("Weights", coin_weights.eval())
print("Volatility: {:.2f}%".format(portfolio_volatility.eval() * 100))
print("")
return coin_weights.eval()
weights = minimize_volatility()
pretty_weights = pd.DataFrame(weights * 100, index = coins, columns = ["Weight %"])
pretty_weights
第一个块定义了产生全局波动率的数学运算。coin_weights是一个k ×1变量数组,默认情况下分配相等的数量,从中得到加权标准偏差数组:
coin_weights = tf.Variable(np.full((len(coins), 1), 1.0 / len(coins)))
weighted_std_devs = tf.multiply(coin_weights, std_deviations)
有了它,我们可以分三步描述矩阵乘法:
product_1 = tf.transpose(weighted_std_devs)
product_2 = tf.matmul(product_1,correlation_matrix)
portfolio_variance = tf.matmul(product_2,weighted_std_devs)
portfolio_volatility = tf.sqrt(portfolio_variance)
请记住,这些说明还没有计算任何东西。它们创建了一个操作图,当我们在会话中运行它们时将执行这些操作。
运行portfolio_volatility将触发依赖portfolio_variance张量,它将使用weighted_std_devs并触发依赖product_2 ,等等。
接下来,我们初始化变量并定义在每个训练步骤中应使用哪个优化器来生成解决方案:
init = tf.global_variables_initializer()
train_step = tf.train.GradientDescentOptimizer(learn_rate).minimize(portfolio_volatility)
最后,我们创建一个会话,运行变量初始化程序操作,并使用我们刚刚定义的操作训练优化器。
with tf.Session() as sess:
sess.run(init)
for i in range(steps):
sess.run(train_step)
需要注意的是train_step取决于 .minimize(portfolio_volatility) ,依赖于我们之前定义的矩阵乘法。在优化程序coin_weights使用新值填充变量之后,将在每个步骤评估这些操作。
那么让我们看看我们得到了什么:
到目前为止还没有什么大的惊喜。我们从分配的每个资产的1 / k(大约14%)开始,但是优化器很快就知道,最小化波动的方法是…根本不分配任何东西。最终权值会收敛到零。
很明显,我们必须强制取值范围从0到1,使它们的和等于1。
在Tensorflow中,我们不能给变量赋值,比如var = val。值可以通过tf.constant(…)来声明,作为具有默认值的tf.Variable(…)来声明,也可以通过my_tensor.assign(…)来分配,它会创建一个需要运行的操作。
让我们添加coin_weights约束并在每个训练步骤后运行它们:
# Optimize weights to minimize volatility
def minimize_volatility():
# Define the model
# Portfolio Volatility = Sqrt (Transpose (Wt.SD) * Correlation Matrix * Wt. SD)
coin_weights = tf.Variable(np.full((len(coins), 1), 1.0 / len(coins))) # our variables
weighted_std_devs = tf.multiply(coin_weights, std_deviations)
product_1 = tf.transpose(weighted_std_devs)
product_2 = tf.matmul(product_1, correlation_matrix)
portfolio_variance = tf.matmul(product_2, weighted_std_devs)
portfolio_volatility = tf.sqrt(tf.reduce_sum(portfolio_variance))
# Constraints: sum([0..1, 0..1, ...]) = 1
lower_than_zero = tf.greater( np.float64(0), coin_weights )
zero_minimum_op = coin_weights.assign( tf.where (lower_than_zero, tf.zeros_like(coin_weights), coin_weights) )
greater_than_one = tf.greater( coin_weights, np.float64(1) )
unity_max_op = coin_weights.assign( tf.where (greater_than_one, tf.ones_like(coin_weights), coin_weights) )
result_sum = tf.reduce_sum(coin_weights)
unity_sum_op = coin_weights.assign(tf.divide(coin_weights, result_sum))
constraints_op = tf.group(zero_minimum_op, unity_max_op, unity_sum_op)
# Run
learning_rate = 0.01
steps = 5000
init = tf.global_variables_initializer()
# Training using Gradient Descent to minimize variance
optimize_op = tf.train.GradientDescentOptimizer(learning_rate).minimize(portfolio_volatility)
with tf.Session() as sess:
sess.run(init)
for i in range(steps):
sess.run(optimize_op)
sess.run(constraints_op)
if i % 2500 == 0 :
print("[round {:d}]".format(i))
print("Weights", coin_weights.eval())
print("Volatility: {:.2f}%".format(portfolio_volatility.eval() * 100))
print("")
sess.run(constraints_op)
return coin_weights.eval()
weights = minimize_volatility()
pretty_weights = pd.DataFrame(weights * 100, index = coins, columns = ["Weight %"])
pretty_weights
运行zero_minimum_op会触发将0分配给coin_weights元素,而unity_max_op通过将大于1的数字替换为1来执行同样的操作。unity_sum_op确保所有值的和等于1。
最后,constraint .run(constraints_op)将这三个操作组合为一个,触发操作组。
再一次,不足为奇。USDT是稳定的,它与美元的价格直接相关。它可能有微小的波动,但很明显,优化器知道,最小的波动是通过把所有鸡蛋放在稳定的篮子。
Stablecoins允许投资者把钱放在加密钱包中,不需要兑换成法定货币。在熊市的情况下,当趋势逆转时,卖出USDT并再次进场显然是有道理的。
但是如果我们观察累积回报我们可以看到所有非稳定的都有很好的正回报,我们不想错过它们。因此,我们刚刚编写了我们的第一个投资组合优化器,但为了使它有用,我们需要在等式中加入回报,而不仅仅是波动性。
夏普比率是现代投资组合理论中最常用的指标之一。它把我们想要的两个量结合成一个简单的公式:
如果rp是投资组合的回报,则rf是无风险利率,而sigma是std偏差。
在加密中,无风险利率将与USDT等稳定币的回报相对应,但由于其长期平均收益为零,我们的公式可以简化为:
让我们使用Python描述夏普比率,以便我们对其进行优化:
# Optimize weights to maximize return/risk
def maximize_sharpe_ratio():
# Define the model
# 1) Variance
coin_weights = tf.Variable(np.full((len(coins), 1), 1.0 / len(coins))) # our variables
weighted_std_devs = tf.multiply(coin_weights, std_deviations)
product_1 = tf.transpose(weighted_std_devs)
product_2 = tf.matmul(product_1, correlation_matrix)
portfolio_variance = tf.matmul(product_2, weighted_std_devs)
portfolio_volatility = tf.sqrt(tf.reduce_sum(portfolio_variance))
# 2) Return
returns = np.full((len(coins), 1), 0.0) # same as coin_weights
for coin_idx in range(0, len(coins)):
returns[coin_idx] = cumulative_returns[coins[coin_idx]]
portfolio_return = tf.reduce_sum(tf.multiply(coin_weights, returns))
# 3) Return / Risk
sharpe_ratio = tf.divide(portfolio_return, portfolio_volatility)
# Constraints
# etc.
sharpe_ratio现在是我们想要最大化的目标。但是,我们使用的优化器仅支持最小化,因此我们将通过最小化其负值来实现相同的目标:
Optimizer(...).minimize(tf.negative(sharpe_ratio))
我们还可以用随机值来初始化coin_weights,而不是使用相等的资本份额:
coin_weights = tf.Variable(tf.random_uniform((len(coins), 1), dtype=tf.float64))
准备好了,让我们在循环中打印进度,看看:
if i % 2000 == 0 :
print("[round {:d}]".format(i))
# print("Coin weights", sess.run(coin_weights)) # if needed
print("Volatility: {:.2f} %".format(portfolio_volatility.eval() * 100))
print("Return {:.2f} %".format(sess.run(portfolio_return)*100))
print("Sharpe ratio", sess.run(sharpe_ratio))
print("")
# ...
因此,考虑到自2017年中期以来的行为,优化者告诉我们投资约80%的Stellar Lumens并将其余部分分散到以太坊,Litecoin,Iotta,Ripple和USDT,过去的回报为6倍,波动率为0.1%。
工具允许根据一组资产的过去表现找到最佳投资组合。机器学习并不是为了找到问题的绝对和准确答案。它是在非常合理的时间内找到近似近似值的非常好的工具。
重要的是要记住,投资组合优化工具只是一种评估工具,而不是“一个真正的答案”。每个结果都必须由人进行分析和解释。我们绝对需要检查每个coin的价格图表和市值,并在进入之前进行足够的基本分析(新闻,路线图,团队等)。