Python预测基金净值:LSTM模型有点香

Python预测基金净值:LSTM模型有点香

  • 搭建LSTM神经网络预测基金净值
    • 一、LSTM简述
    • 二、继续爬基金,生成训练集、验证集、测试集文件
    • 三、建模,读入数据,训练,验证
    • 四、看图
  • 总结
  • 后记

搭建LSTM神经网络预测基金净值

上一篇《Python预测基金净值:keras神经网络》搭建了一个简单神经网络用来预测基金净值。

最后得出什么结论?
有点臭。

看看训练情况:
Epoch 1/100
156/156 [= = = = =] - 0s 3ms/step - loss: 0.0450 - val_loss: 0.1114

Epoch 100/100
156/156 [= = = = =] - 0s 2ms/step - loss: 0.0299 - val_loss: 0.1056

经过100次训练,训练集的loss已经从0.0450降到0.0299,但是验证集的val_loss几乎么有改善(说明训练集的“改善”只是过拟合)。这对于一个神经网络而言,相当于死刑判决:模型或参数有大问题。

事实上,对于股票/指数/基金净值这些“有序”数据而言,应该采用循环神经网络来搭建,本篇就此试一试。

初步结论是:有点香。

一、LSTM简述

循环神经网络(Recurrent Neural Network, RNN)是以序列数据为输入,在序列的演进方向进行循环递归的递归神经网络。RNN适用于处理时间顺序、逻辑顺序或其他序列特性顺序的数据。

RNN的单元结构如下图(左)所示,上一时刻输出ht-1与当前时刻输入Xt进行拼接,然后由神经网络(以tanh为激活函数)进行处理,得到输出ht。

Python预测基金净值:LSTM模型有点香_第1张图片
后续研究发现RNN存在“长期依赖问题”(long-term dependencies problem),亦即在学习过程中,会出现梯度消失或梯度爆炸现象。

LSTM比较好地解决了这个问题,其单元结构如上图(中)所示,增加了一个t时刻的细胞状态Ct,并且增加了三个门(忘记门、输入门、输出门),即图中标注为σ(sigma)的3个以sigmoid作为激活函数的神经网络。如果去掉Ct和三个门,如上图(右)所示,则与上图(左)的RNN等效。

关于LSTM,可参见:理解 LSTM 网络 by 朱小虎

关于keras,可参见:keras中文文档

二、继续爬基金,生成训练集、验证集、测试集文件

import requests
import time
import execjs

fileTrain = './data/accTrain.csv'
jjTrain = ['004609', '004853', '005524', '005824', '007749']
fileTest = './data/accTest.csv'
jjTest = '007669'

def getUrl(fscode):
    head = 'http://fund.eastmoney.com/pingzhongdata/'
    tail = '.js?v='+ time.strftime("%Y%m%d%H%M%S",time.localtime())
    return head+fscode+tail

# 根据基金代码获取净值
def getWorth(fscode):
    content = requests.get(getUrl(fscode))
    jsContent = execjs.compile(content.text)
    #累计净值走势
    ACWorthTrend = jsContent.eval('Data_ACWorthTrend')
    ACWorth = []
    for dayACWorth in ACWorthTrend:
        ACWorth.append(dayACWorth[1])
    return ACWorth

ACWorthFile = open(fileTrain, 'w')
for code in jjTrain:
    try:
        ACWorth = getWorth(code)
    except:
        continue    
    if len(ACWorth) > 0:
        ACWorthFile.write(",".join(list(map(str, ACWorth))))
        ACWorthFile.write("\n")
        print('{} data downloaded'.format(code))
ACWorthFile.close()

ACWorthTestFile = open(fileTest, 'w')
ACWorth = getWorth(jjTest)
if len(ACWorth) > 0:
    ACWorthTestFile.write(",".join(list(map(str, ACWorth))))
    ACWorthTestFile.write("\n")
    print('{} data downloaded'.format(jjTest))
ACWorthTestFile.close()

如上一篇所述,‘004609’, ‘004853’, ‘005524’, ‘005824’, '007749’是5只目前收益较稳定的偏债型混合基金。爬取每日净值数据,作为训练集和验证集(通过设置validation_split=0.25)的数据文件。'007669’也是一只同类型的基金,上一篇没选它,是因为它目前在支付宝基金里面暂停代购。这次用作测试集的数据文件。

注意,和上一篇相比,代码改动了一点:基金数据按日期正序保存。

三、建模,读入数据,训练,验证

import numpy as np
import pandas as pd
import csv
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from matplotlib import pyplot as plt

plt.rcParams['font.sans-serif']='SimHei'
plt.rcParams['axes.unicode_minus']=False

batch_size = 4
epochs = 50
time_step = 6 #用多少组天数进行预测
input_size = 6 #每组天数,亦即预测天数
look_back = time_step * input_size
showdays = 120 #最后画图观察的天数(测试天数)

X_train = []
y_train = []
X_validation = []
y_validation = []
testset = [] #用来保存测试基金的近期净值

#忽略掉最近的forget_days天数据(回退天数,用于预测的复盘)
forget_days = 0

def create_dataset(dataset):
    dataX, dataY = [], []
    print('len of dataset: {}'.format(len(dataset)))
    for i in range(0, len(dataset) - look_back, input_size):
        x = dataset[i: i + look_back]
        dataX.append(x)
        y = dataset[i + look_back: i + look_back + input_size]
        dataY.append(y)
    return np.array(dataX), np.array(dataY)

def build_model():
    model = Sequential()
    model.add(LSTM(units=128, input_shape=(time_step, input_size)))
    model.add(Dense(units=input_size))
    model.compile(loss='mean_squared_error', optimizer='adam')
    return model

# 设定随机数种子
seed = 7
np.random.seed(seed)

# 导入数据(训练集)
with open(fileTrain) as f:
    row = csv.reader(f, delimiter=',')
    for r in row:
        dataset = []
        r = [x for x in r if x != 'None']
        #涨跌幅是2天之间比较,数据会减少1个
        days = len(r) - 1
        #有效天数太少,忽略
        if days <= look_back + input_size:
            continue
        for i in range(days):
            f1 = float(r[i])
            f2 = float(r[i+1])
            if f1 == 0 or f2 == 0:
                dataset = []
                break
            #把数据放大100倍,相当于以百分比为单位
            f2 = (f2 - f1) / f1 * 100
            #如果涨跌幅绝对值超过15%,基金数据恐有问题,忽略该组数据
            if f2 > 15 or f2 < -15:
                dataset = []
                break
            dataset.append(f2)
        n = len(dataset)
        #进行预测的复盘,忽略掉最近forget_days的训练数据
        n -= forget_days
        if n >= look_back + input_size:
            #如果数据不是input_size的整数倍,忽略掉最前面多出来的
            m = n % input_size
            X_1, y_1 = create_dataset(dataset[m:n])
            X_train = np.append(X_train, X_1)
            y_train = np.append(y_train, y_1)

# 导入数据(测试集)
with open(fileTest) as f:
    row = csv.reader(f, delimiter=',')
    #写成了循环,但实际只有1条测试数据
    for r in row:
        dataset = []
        #去掉记录为None的数据(当天数据缺失)
        r = [x for x in r if x != 'None']
        #涨跌幅是2天之间比较,数据会减少1个
        days = len(r) - 1
        #有效天数太少,忽略,注意:测试集最后会虚构一个input_size
        if days <= look_back:
            print('only {} days data. exit.'.format(days))
            continue
        #只需要最后画图观察天数的数据
        if days > showdays:
            r = r[days-showdays:]
            days = len(r) - 1
        for i in range(days):
            f1 = float(r[i])
            f2 = float(r[i+1])
            if f1 == 0 or f2 == 0:
                print('zero value found. exit.')
                dataset = []
                break
            #把数据放大100倍,相当于以百分比为单位
            f2 = (f2 - f1) / f1 * 100
            #如果涨跌幅绝对值超过15%,基金数据恐有问题,忽略该组数据
            if f2 > 15 or f2 < -15:
                print('{} greater then 15 percent. exit.'.format(f2))
                dataset = []
                break
            testset.append(f1)
            dataset.append(f2)
        #保存最近一天基金净值
        f1=float(r[days])
        testset.append(f1)
        #测试集虚构一个input_size的数据(若有forget_days的数据,则保留)
        if forget_days < input_size:
            for i in range(forget_days,input_size):
                dataset.append(0)
                testset.append(np.nan)
        else:
            dataset = dataset[:len(dataset) - forget_days + input_size]
            testset = testset[:len(testset) - forget_days + input_size]
        if len(dataset) >= look_back + input_size:
            #将testset修正为input_size整数倍加1
            m = (len(testset) - 1) % input_size
            testset = testset[m:]
            m = len(dataset) % input_size
            #将dataset修正为input_size整数倍
            X_validation, y_validation = create_dataset(dataset[m:])

#将输入转化成[样本数,时间步长,特征数]
X_train = X_train.reshape(-1, time_step, input_size)
X_validation = X_validation.reshape(-1, time_step, input_size)

#将输出转化成[样本数,特征数]
y_train = y_train.reshape(-1, input_size)
y_validation = y_validation.reshape(-1, input_size)

print('num of X_train: {}\tnum of y_train: {}'.format(len(X_train), len(y_train)))
print('num of X_validation: {}\tnum of y_validation: {}'.format(len(X_validation), len(y_validation)))

# 训练模型
model = build_model()
model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, verbose=1, validation_split=0.25, shuffle=True)

# 评估模型
train_score = model.evaluate(X_train, y_train, verbose=0)
validation_score = model.evaluate(X_validation, y_validation, verbose=0)

# 预测
predict_validation = model.predict(X_validation)

#将之前虚构的最后一组input_size里面的0涨跌改为NAN(不显示虚假的0if forget_days < input_size:
    for i in range(forget_days,input_size):
        y_validation[-1, i] = np.nan

print('Train Set Score: {:.3f}'.format(train_score))
print('Test Set Score: {:.3f}'.format(validation_score))
print('未来{}天实际百分比涨幅为:{}'.format(input_size, y_validation[-1]))
print('未来{}天预测百分比涨幅为:{}'.format(input_size, predict_validation[-1]))

#进行reshape(-1, 1)是为了plt显示
y_validation = y_validation.reshape(-1, 1)
predict_validation = predict_validation.reshape(-1, 1)
testset = np.array(testset).reshape(-1, 1)

# 图表显示
fig=plt.figure(figsize=(15,6))
plt.plot(y_validation, color='blue', label='基金每日涨幅')
plt.plot(predict_validation, color='red', label='预测每日涨幅')
plt.legend(loc='upper left')
plt.title('关联组数:{}组,预测天数:{}天,回退天数:{}天'.format(time_step, input_size, forget_days))
plt.show()

代码稍稍比上一篇复杂一点,建议先看懂上一篇(让上一篇“臭文”发挥余热)。

几个参数介绍一下:
input_size:即作为时间窗口大小,同时也是LSTM神经网络的输入参数之一,以及输出参数(多对多的模式)。

time_step:时间窗口组数,即用多少组维度为input_size的数据,来预测下一个input_size的数据。

look_back:等于time_step * input_size,相当于使用多少个现有数据来预测,LSTM网络的input_shape是也。

showdays:其实就是测试集大小,按说可以大一些,但太大了画图看不清,建议用120吧。

forget_days:回退天数,用于复盘前forget_days天的预测。

以上代码采用:time_step=6,input_size=6(因此LSTM的units设为128,通常建议为input_shape的2-3倍),运行之:

训练速度很快,打印信息参考如下:
num of X_train: 434 num of y_train: 434
num of X_validation: 15 num of y_validation: 15
Epoch 1/50
56/56 [= = = = =] - 0s 20ms/step - loss: 0.0482 - val_loss: 0.0956

Epoch 50/50
56/56 [= = = = =] - 0s 4ms/step - loss: 0.0100 - val_loss: 0.0431
Train Set Score: 0.018
Test Set Score: 0.045

未来6天预测百分比涨幅为:[-0.05394792 -0.4340353 0.20469473 0.15502352 0.0738658 0.1280248 ]

和上一篇“臭文”相比,至少val_loss训练有效果了。而且最后得分也不错。

当然,结果到底如何,还得看图说话。

四、看图

Python预测基金净值:LSTM模型有点香_第2张图片
Python预测基金净值:LSTM模型有点香_第3张图片
Python预测基金净值:LSTM模型有点香_第4张图片
Python预测基金净值:LSTM模型有点香_第5张图片
Python预测基金净值:LSTM模型有点香_第6张图片
Python预测基金净值:LSTM模型有点香_第7张图片
凭直觉,look_back为30多天(相当于1.5个月的交易日)较为合理。采用不同的参数组合(time_step/input_size),跑几次,可见预测图形和实际图形的拟合度,和上一篇相比,有了极大改善。

感觉“有点香”了。

最后再加一段看净值图形的代码:(上一篇这一段写差了点,这次改了)

# 实际净值、预测净值
y_validation_plot = np.empty_like(testset)
predict_validation_plot = np.empty_like(testset)
y_validation_plot[:, :] = np.nan
predict_validation_plot[:, :] = np.nan

y = testset[look_back, 0]
p = testset[look_back, 0]
for i in range(look_back, len(testset)-1):
    y *= (1 + y_validation[i-look_back, 0] / 100)
    p *= (1 + predict_validation[i-look_back, 0] / 100)
    #print('{:.4f} {:.4f} {:.4f}'.format(testset[i+1,0], y, p))
    y_validation_plot[i, :] = y
    predict_validation_plot[i, :] = p

# 图表显示
fig=plt.figure(figsize=(15,6))
plt.plot(y_validation_plot, color='blue', label='基金每日净值')
plt.plot(predict_validation_plot, color='red', label='预测每日净值')
plt.legend(loc='upper left')
plt.title('关联组数:{}组,预测天数:{}天,回退天数:{}天'.format(time_step, input_size, forget_days))
plt.show()

由于基金净值是连续乘出来的,10天里面就算9天预测相同,但另一天有些差异(比如实际爆涨1.3%,而预测只是大涨0.5%),那么净值就会一直有较大的差异了------差不多要一直差1分多钱了,所以只贴一张图作为示意(这一段其实是为了验证代码是否正确计算了净值------把print语句打开)。

Python预测基金净值:LSTM模型有点香_第8张图片

总结

LSTM模型效果有了明显改进。

当然,这里面有一些“作弊”成分:作为测试集的007669基金,由于跟训练集的5个基金相似(都是稳健的偏债混合基金),因此大的起伏基本一致,能拟合也不足为奇(既然验证集的val_loss经训练有了明显改进,测试集007669当然也可以表现不错------事实上,测试集分数,与验证集的val_loss分数相当)。

但是,如果认为LSTM模型只是靠“作弊”得分,那么这篇文章我就白写,您也白看了。LSTM本身是有“预测”能力的:不妨设置forget_days进行回退,复盘之前的预测。

后记

1、该模型可以用来预测股票指数吗?
当然可以,只需改动两行代码------先把测试基金007669改为股票指数基金比如005918(天弘沪深300ETF联接C),再把训练集数据放大100倍的地方,改为放大400倍(训练集是偏债基金,股票占比大约四分之一,涨跌幅度大约是测试集股指基金的四分之一),就可以啦。效果如下图所示:
Python预测基金净值:LSTM模型有点香_第9张图片
2、time_step和input_size建议都用6天。
原因:一是基金持有7天(亦即5个交易日)则卖出手续费较低,预测后续6天走势作为参考是合理的(再长则没意义);二是感觉这种时间序列,time_step和input_size相接近是合理的。

3、进一步改进,采用层叠LSTM(第一层的ht作为第二层的Xt,需设参数return_sequences=True),例如改这几行代码:

#epochs = 50
epochs = 200

    #model.add(LSTM(units=128, input_shape=(time_step, input_size)))
    #model.add(Dense(units=input_size))
    model.add(LSTM(units=128, return_sequences=True, input_shape=(time_step, input_size)))
    model.add(LSTM(units=32))
    model.add(Dense(units=input_size))

又或者这样:

#epochs = 50
epochs = 200

    #model.add(LSTM(units=128, input_shape=(time_step, input_size)))
    #model.add(Dense(units=input_size))
    model.add(LSTM(units=128, return_sequences=True, input_shape=(time_step, input_size)))
    model.add(LSTM(units=32, return_sequences=True))
    model.add(Dense(units=1))

注意:后者这个模型仅当time_step = input_size才可以,因为它最后全连接层的输出维数是(time_step, 1),不是(input_size)。感觉该模型不够合理。

4、将LSTM模型改为GRU(门控循环单元),也是可以的。改两处代码:

#from keras.layers import LSTM
from keras.layers.recurrent import GRU

    #model.add(LSTM(units=128, input_shape=(time_step, input_size)))
    model.add(GRU(units=128, input_shape=(time_step, input_size)))

测试结果,层叠LSTM似有提高,而GRU没什么改善。

5、由于基金净值通常在工作日19:00—21:00进行更新,因此需避免在傍晚下载基金净值进行训练。

6、当然,不可以用于预测具体股票,切记切记!勿谓言之不预也。

这三篇就告一段落了,有缘人请耐心研究收获(请勿剽窃,更勿招摇),实现小目标,呵呵。

你可能感兴趣的:(Python,python,神经网络,深度学习,lstm)