前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深度 | 强化学习应用金融投资组合优化(附代码)

深度 | 强化学习应用金融投资组合优化(附代码)

作者头像
量化投资与机器学习微信公众号
发布2019-02-26 16:20:41
3.6K0
发布2019-02-26 16:20:41
举报

本期作者:Tal Perry

作者介绍:Nevermore

前言

首先我们要声明。我们本着分享的精神把所有内容进行分享,并希望大家能从中学习。

Everything here is probably wrong, and you should trust none of it

我想要Agent做什么

我们建立一个观察N只股票的Agent,并在每一阶段根据决策分配给每只股票一定比例的资金。我们希望Agent能够在符合股票市场真实场景假设下做到这一点,例如:有(实质性的)交易成本,股票不遵循标准正态分布等。我们不知道其他的市场参与者所拥有信息的多少等(换句话说,Agent可以处理POMDP模型中尽可能多的PO)。

互联网上很难找到有关金融方面强化学习的资料。很多资料都是非常琐碎的。更好的材料来自于学术界,(我们被很多论文的启发,但是这一篇我觉得比其他的都要好)。

http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.1.7210&rep=rep1&type=pdf

为了理解一篇关于应用强化学习构建最优投资组合的论文,你需要有足够的知识来知道什么是最优投资组合和全面的强化学习知识,用来理解作者是如何使用这种技术的。最好是试图将这两个不同领域(金融和计算机)的知识结合起来。

我们希望很通俗的去解释这些问题,但是如果你有一些RL和金融的概念,可能会更容易理解。我们会试用通俗的语言解释高大上的问题,但是强化学习是核心

数据

金融数据是需要花钱购买的。为了满足我们的好奇心,我们先决定自己生成模拟数据。这样可以更好地控制问题的难易程度,然后从简单的情况入手,最后慢慢深入问题。

我们写了一个能够模拟股票市场的magic函数。生成10只股票数据。

获取全部代码,见文末

代码语言:javascript
复制
%load_ext autoreload
%autoreload 2
cd ../acfl/

from env.Env import Env
from env.priceGenerator import make_stock
from learners.a2c import PolicyEstimator,ValueEstimator, reinforce
import tensorflow as tf

def sin_func(num_stocks,length):
    a = np.array(range(length+1))
    a.shape= [length+1,1]
    a = a/(2*np.pi)
    a = np.sin(a)
    return a+5.0001

D = pd.DataFrame(make_stock(1000,10)*np.random.uniform(2,4,10))
plt.xkcd()
D.plot(figsize=(20,10),title='My make beleive market for 10 stocks')

我们认为这个市场比实际市场更容易设计,一些股票显然是相关的,并且有足够强的自相关性。这是经过设计的,最终将模拟更加复杂的市场,但最好先看看算法,可以先学到一些基本的思路,以便让你相信和理解这样一个magic函数,下面是模拟生成的另一个市场。

这是生成这些模拟股票的代码:

获取全部代码,见文末

代码语言:javascript
复制
import numpy as np
def make_stock(length=100, num_stocks=2):
    alpha = 0.9
    k = 2
    cov = np.random.normal(0, 5, [num_stocks, num_stocks])
    cov = cov.dot(cov.T) # This is a positive semidefinite matrix, e.g. a covariance matrix
    A = np.random.multivariate_normal(np.zeros(num_stocks), cov, size=[length]) # sample noise, with covariance 
    B = np.random.multivariate_normal(np.zeros(num_stocks), cov, size=[length]) # sample another noise, with covariance
    bs = [np.zeros(shape=num_stocks)] # 
    ps = [np.zeros(shape=num_stocks)] # The prices

    for a, b in zip(A, B):
        bv = alpha * bs[-1] + b # calculate some trend
        bs.append(bv)
        pv = ps[-1] + bs[-2] + k * a # Previosu price + previous trend factor, plus some noise
        ps.append(pv)

    #     ps = [0]
    #     for a,b,common in zip(A,BB,commonNoise):
    #         ps.append(ps[-1]+b+k*a+2*common)
    #     P = np.array(ps)
    #     P = np.exp(P/(P.max()-P.min()))
    ps = np.array(ps).T # reshape it so that its [length,stocks] 
    R = ps.max(1) - ps.min(1) # Scale factor
    prices = np.exp(ps.T / (R)) *np.random.uniform(10,250,num_stocks) # Normalize, exponantiate then make the prices more varied
    return prices

我们最开始只使用了正弦波来看看否可以让模型进行强化学习。一旦程序能够运行了,就会继续注入一点噪音。这个思想是非常有价值的,这样操作会更容易在一个简单确定的环境中找到bug,然后再从更复杂的加上随机数学的代码寻找类似的bug。

建立RL环境

强化学习是关于一个Agent与环境之间进行的互动,就像詹姆斯邦德加入绿色和平组织一样。这是一种奇特的说法,即环境告诉Agent外部世界现在的状态是什么,Agent说要做什么,然后Agent获得一份奖励,并利用它来改进。

建立环境可以让你扮演上帝的角色,从而让你意识到这个世界为什么如此混乱。要把一切都弄好是很难的,很容易在这种环境中犯下小错误从而毁掉一切的,并且这种小错误一般很难找到。

总之,在我们扮演上帝的角色中,模拟了如下的世界:环境告诉Agent每种股票的当前价格价格、一定回测期的历史价格和当前的投资组合。Agent反馈它想要加入更新的新投资组合。

然后环境在时间上进入下一节点,通过买卖构造出Agent所提出的投资组合,并考虑交易成本。这其中有些人为操作的思想,因为Agent总是处理帐户的百分比,而环境必须将其转换为美元和股票,并需要调整四舍五入、承担交易成本,并由于舍入错误而返还资金(否则我们将得到负的奖励)。

下面的代码,用于构造上述的环境:

获取全部代码,见文末

代码语言:javascript
复制
import numpy as np
from collections import  defaultdict

from env.priceGenerator import make_stock

costPerShare = 0 # 0.01
class Env:
    '''
    A simple environemnt for our agent,
    the action our agent gives is  weighting over the stocks + cash
    the env calcutes that into stock and figures out the returns
    '''
    def __init__(self,price_fn,num_stocks=2,length=2,starting_value=1000,lookback=10):
        '''
        :param price_fn:  A function that returns a numpy array of prices
        :param num_stocks: How many stocks in our univerese
        :param length: The length of an episode
        '''
        self.num_stocks = num_stocks
        self.lookback = lookback
        self.length = length
        self.oprices= price_fn(num_stocks=num_stocks,length=length)
        self.prices = np.concatenate([self.oprices,np.ones([length+1,1])],axis=1) #attach the value of cash
        self.portfolio = np.zeros([num_stocks+1]) #2k and 2k+1 are te long and short of a stock. portfolio[-1] is cash
        self.portfolio[-1] = 1
        self.time =0
        self.__account_value = starting_value
        self.__shares=np.array([0]*num_stocks +[starting_value])
        self.hist = defaultdict(list)
    @property
    def shares(self):
        return self.__shares
    @property
    def account_value(self):
        return self.__account_value
    @shares.setter
    def shares(self,new_shares):

        self.__shares = new_shares
        self.hist['shares'].append(self.shares)
    @account_value.setter
    def account_value(self,new_act_val):
        self.__account_value = new_act_val
        try:
            act_returns  = self.account_value / self.hist['act_val'][-1]
        except:
            act_returns =1
        self.hist['act_val'].append(self.account_value)
        self.hist['act_returns'].append(act_returns)

    def step(self,new_portfolio):
        '''
        Get the next prices. Then transition the value of the account into the desired portfolio
        :param new_portfolio:
        :return:
        '''
        self.time +=1

        self.update_acount_value(new_portfolio)
        reward = np.log(self.hist['act_returns'][-1]) #already includes transaction costs
        state = {
            "prices":self.prices[self.time-self.lookback+1:self.time+1,:-1], # All prices upto now inclusive but no cash
            "portfolio":self.portfolio,

        }
        done = self.time >=len(self.prices)-2
        return state,reward,done

    def update_acount_value(self,new_portfolio):
        currentShareValues = self.shares * self.prices[self.time]
        currentAccountValue = sum(currentShareValues)

        currentPortfolioProportions = currentShareValues / currentAccountValue
        desiredCashChange = (new_portfolio -currentPortfolioProportions )* currentAccountValue

        desiredChangeInShares = np.floor(desiredCashChange / self.prices[self.time])
        self.shares = self.shares + desiredChangeInShares
        newAccountValue = np.sum(self.shares*self.prices[self.time])
        #becuse we take the floor, sometimes we lose cash for no reason. This is a fix
        missingCash  = currentAccountValue - newAccountValue
        transactionCost = sum(np.abs(desiredChangeInShares[:-1])*costPerShare)

        self.shares[-1] += missingCash - transactionCost

        transactionCost = sum(np.abs(desiredChangeInShares[:-1])*costPerShare)
        self.hist["changeInShares"].append(desiredChangeInShares)
        self.hist["transactionCosts"].append(transactionCost)

        self.account_value =np.sum(self.shares*self.prices[self.time])

实际学习的东西

强化学习能分成两种主流的方法,Policy方法和Value方法。其中,Policy方法决定模型下一步应该做什么,Value方法确定最好的决策是什么。通常比较流行并且效果较好的方法是将这两者混合起来,并且加上现代的Actor批判方法。

我们认为Value的估计在金融上的应用是有问题的,除非你设置一个非常聪明的模型来定义收益回报。但是如果你没有深入研究透彻,使用收益或风险调整后的收益作为奖励,那么这是一个非常嘈杂的信号。这就是为什么我们怀疑在这种情况下的Value函数。(感兴趣的读者可以阅读QLBS论文,该论文用一种Value函数进行了完美的实现)

https://arxiv.org/pdf/1712.04609.pdf

有趣的是,当我们研究由正弦波产生的价格时,Actor-Critic方法(近似于状态值)比pure policy好得多,但是一旦加入噪音更大的数据,它们似乎只会让事情变得更糟而不是更好。

互联网有很多关于强化学习算法的资料,而github上拥有很多算法实现。实际上,这里讨论的代码是在github从Denny Britz的repo中复制的。

https://github.com/dennybritz/reinforcement-learning

此外,虽然RL背后的想法非常深刻,但他们的应用主要是关于良好的工程设计和使问题的现实适应它们。因此,由于这些原因,本节仍然很短。

投资组合操作

我们希望Agent的行为是投资组合在n只股票和现金上的权重(一共n+1个权重)。

互联网上的大多数RL资料上都说,当你想要连续动作时需要从高斯分布中采样;如果你想要多重连续动作时(投资组合的权重),那么你需要从多元高斯分布中采样。

高斯分布函数有着各种不满意的属性,他们就像概率分布中的Kardashians。我们希望为每个股票分配一个权重,介于0和1之间,且所有权重总和恰好为1。此时多元高斯分布只能给出- inf和inf之间的数字,没有约束,这是不令人满意的。事实上,更糟糕的是,高斯分布下softmaxing的输出将是的Agent几乎无法训练。

除此之外,多元高斯分布一般通过各变量及其协方差矩阵的均值进行拟合。要计算这么多的参数,还要编写一些复杂的程序以使深层的输出成为半正定对称矩阵。所以在这两个问题之间,我们宁愿使用一些更具体的方法来解决这个问题。

讨厌Dirichlet函数,喜欢Dirichlet分布

这是因为我么恩在一个关于Dirichlet函数的cal 1中失败了。

http://mathworld.wolfram.com/DirichletFunction.html

幸运的是,有一个更加合适的分布供我们抽样。Dirichlet(K) distribution是K-单纯形上的一个分布。N只股票和现金的投资组合是N + 1 维空间上的一个点,因此从Dirichlet(N + 1)分布中抽样就可以得到一个投资组合。

虽然这个函数不是内置于Tensorflow中,但它内置于Tensorflow Propability中,因此整个实现过程非常简单:

https://www.tensorflow.org/probability/api_docs/python/tfp/distributions/Dirichlet

代码语言:javascript
复制
self.alphas= tf.contrib.layers.fully_connected(
          inputs=l2,
          num_outputs=num_stocks+1,
          activation_fn=tf.nn.relu,
          weights_initializer=tf.initializers.glorot_uniform)

      self.alphas +=1

      self.dirichlet  = tfp.distributions.Dirichlet(self.alphas)
      self.action = self.dirichlet._sample_n(1)
      self.action = tf.squeeze(self.action)

就这么简单。

反思

我们从Dirichlet分布中抽样的想法在理论上是非常棒的,但是在实践中则被证明是一无是处的。有两种现象不很很好。

首先,我们在每一步都采样一个新的分布,这意味着我们正在改变位置。这是低效的,但是当你从分布中进行采样时这是不可避免的。更糟糕的是,我们因为调整投资组合获得了负的收益回报(交易成本),但没有机制维持原有的投资组合权重不变。

其次,在每一步采样新的投资组合看起来是愚蠢的。我们认为应该对改变投资组合的决策进行抽样,然后在必要时对投资组合进行抽样。这样可以使用分布的方差(或方差向量的范数)来做出这个决策。

这是我们目前最感兴趣的问题。总之,它在数学上做到了我想要的,但是在实践上却做得一塌糊涂。

其他问题

另一个问题是赋予模型的奖励。目前Reward只是账户的对数收益(股票价值的变化 - 交易成本)。我们认为在多股票环境中这一方式噪声过多了,但即使在单只股票正弦波的环境中也是一个不好的收益定义。Agent了解到最简单的赚钱方法是购买市场上涨最大的股票。

同样,可能有很多bug。我们纯粹是为了好玩而编写了这段代码且没有编写测试,讲真,RL是一个极其需要测试的领域。每个bug都可能很糟糕,因为通常你不知道它是一个错误还是要求比你预期更复杂的系统。它会暴露你每个工程上的坏习惯和不正确的假设。

未来

讲真,如果这是一台印钞机,我们就不会把它放在互联网上了。我们认为交易员确实可以寻找合适的应用RL取赚钱的途径,但是这一过程中还有很多工作要做,如果你能够执行它,你可能有更好的选择。

然而,我们发现这是非常有趣的。金融领域有很多问题,深度强化学习能让你从有趣的角度看待它们。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-02-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 量化投资与机器学习 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Everything here is probably wrong, and you should trust none of it
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档