LSTM Networks应用于股票市场之Functional Model

策略分享
lstm
标签: #<Tag:0x00007f4cd465df10> #<Tag:0x00007f4cd465ddd0>

(iQuant) #1

LSTM Networks应用于股票市场之Functional Model。本文是已初步探索,如下示例中 使用 LSTM 预测沪深300 涨跌。

  • 用一个input(6 features * 30 time series)训练LSTM,将训练结果与另一个辅助性输入label(np.round(close/500))一起作为input输入至Dense层
  • LSTM future_return_5作为output(time series=30,features=[‘close’,‘open’,‘high’,‘low’,‘amount’,‘volume’])
克隆策略
In [7]:
# 0. 基础参数配置
class lstm_conf:
    # 股票代码:用LSTM预测沪深300指数
    instrument = '000300.SHA'
    # 设置用于训练和回测的开始/结束日期
    start_date = '2005-01-01'
    split_date = '2015-01-01'
    end_date = '2017-05-01'
    fields = ['close', 'open', 'high', 'low', 'amount', 'volume']
    # 每个input的长度,使用过去30天的数据
    feature_back_days = 30
    # 指定进行梯度下降时每个batch包含的样本数,训练时一个batch的样本会被计算一次梯度下降,使目标函数优化一步
    batch_size = 100
In [8]:
# 1. 加载数据
def load_and_label_data(instrument, start_date, end_date, fields):
    df = D.history_data(instrument, start_date, end_date, fields)
    # 只保留有数据的交易日
    df = df[df.amount>0]

    # 计算收益:以明日开盘价买入,第五天的收盘价卖出
    df['return'] = df['close'].shift(-5) / df['open'].shift(-1) - 1
    #去极值
    df['return'] = df['return'].clip(-0.2, 0.2) 
    # 适当增大return范围,利于LSTM模型训练
    df['return'] = df['return'] * 10

    # 辅助输入
    df['label'] = np.round(df['close'] / 500)

    df.dropna(inplace=True)
    df.reset_index(drop=True, inplace=True)

    return Outputs(data=DataSource.write_df(df))

lstm_m1 = M.cached.v2(run=load_and_label_data, kwargs=dict(
    instrument=lstm_conf.instrument,
    start_date=lstm_conf.start_date,
    end_date=lstm_conf.end_date,
    fields=lstm_conf.fields
))
[2018-07-23 14:16:59.718502] INFO: bigquant: cached.v2 开始运行..
[2018-07-23 14:16:59.738068] INFO: bigquant: 命中缓存
[2018-07-23 14:16:59.739617] INFO: bigquant: cached.v2 运行完成[0.021189s].
In [9]:
# 2. 生成数据集
def generate_datasets(input_ds, x_fields, feature_back_days):
    from numpy.lib.stride_tricks import as_strided
    from sklearn.preprocessing import scale

    input_df = input_ds.read_df()

    X_data = input_df[x_fields].values
    rows, cols = X_data.shape
    row_stride, col_stride = X_data.strides

    # stride view
    X = as_strided(
        X_data,
        shape=(rows - feature_back_days + 1, feature_back_days, cols),
        strides=(row_stride, row_stride, col_stride))
    # copy x for update
    X = np.array(X)
    X = [scale(x) for x in X]
    X_aux = input_df['label'].values[feature_back_days - 1:]
    Y = input_df['return'].values[feature_back_days - 1:]
    meta_date = input_df['date'].values[feature_back_days - 1:]

    df = pd.DataFrame({
        'date': meta_date,
        'X': X,
        'Y': Y,
        'X_aux': X_aux
    })

    return Outputs(data=DataSource.write_df(df))

lstm_m2 = M.cached.v2(run=generate_datasets, kwargs=dict(
    input_ds=lstm_m1.data,
    x_fields=lstm_conf.fields,
    feature_back_days=lstm_conf.feature_back_days
))
[2018-07-23 14:16:59.852758] INFO: bigquant: cached.v2 开始运行..
[2018-07-23 14:16:59.858556] INFO: bigquant: 命中缓存
[2018-07-23 14:16:59.860097] INFO: bigquant: cached.v2 运行完成[0.007342s].
In [10]:
# 3. 拆分训练数据和测试数据
lstm_m3_train = M.filter.v2(data=lstm_m2.data, expr='date <= "%s"' % lstm_conf.split_date)
lstm_m3_evaluation = M.filter.v2(data=lstm_m2.data, expr='date > "%s"' % lstm_conf.split_date)
[2018-07-23 14:16:59.872883] INFO: bigquant: filter.v2 开始运行..
[2018-07-23 14:16:59.927643] INFO: bigquant: 命中缓存
[2018-07-23 14:16:59.929650] INFO: bigquant: filter.v2 运行完成[0.056758s].
[2018-07-23 14:16:59.932541] INFO: bigquant: filter.v2 开始运行..
[2018-07-23 14:16:59.950812] INFO: bigquant: 命中缓存
[2018-07-23 14:16:59.952140] INFO: bigquant: filter.v2 运行完成[0.019595s].
In [13]:
# 4. LSTM模型训练

# 自定义激活函数
def activation_atan(x):
    import tensorflow as tf
    return tf.atan(x)

def lstm_train(input_ds, batch_size, activation):
    from keras.layers import Input, Dense, LSTM, concatenate
    from keras.models import Model

    # 构建神经网络层 1层LSTM层+3层Dense层
    lstm_input = Input(shape=(30, 6), name='lstm_input')
    lstm_output = LSTM(128, activation=activation, dropout_W=0.2, dropout_U=0.1)(lstm_input)
    aux_input = Input(shape=(1,), name='aux_input')
    merged_data = concatenate([lstm_output, aux_input],axis=-1)
    dense_output_1 = Dense(64, activation='linear')(merged_data)
    dense_output_2 = Dense(16, activation='linear')(dense_output_1)
    predictions = Dense(1, activation=activation)(dense_output_2)

    model = Model(input=[lstm_input, aux_input], output=predictions)

    model.compile(optimizer='adam', loss='mse', metrics=['mse'])

    df = input_ds.read_df()
    model.fit(
        [np.array(df['X'].values.tolist()), np.array(df['X_aux'].values)],
        np.array(df['Y'].values),
        batch_size=batch_size,
        nb_epoch=10,
        verbose=2
    )

    # 保存模型
    model_ds = DataSource()
    model.save(model_ds.open_temp_path())
    model_ds.close_temp_path()

    return Outputs(data=model_ds)

lstm_m4 = M.cached.v2(run=lstm_train, kwargs=dict(
    input_ds=lstm_m3_train.data,
    batch_size=lstm_conf.batch_size,
    activation=activation_atan
))
[2018-07-23 14:18:51.893331] INFO: bigquant: cached.v2 开始运行..
Epoch 1/10
 - 6s - loss: 0.1978 - mean_squared_error: 0.1978
Epoch 2/10
 - 3s - loss: 0.1733 - mean_squared_error: 0.1733
Epoch 3/10
 - 3s - loss: 0.1695 - mean_squared_error: 0.1695
Epoch 4/10
 - 3s - loss: 0.1696 - mean_squared_error: 0.1696
Epoch 5/10
 - 4s - loss: 0.1722 - mean_squared_error: 0.1722
Epoch 6/10
 - 3s - loss: 0.1699 - mean_squared_error: 0.1699
Epoch 7/10
 - 3s - loss: 0.1695 - mean_squared_error: 0.1695
Epoch 8/10
 - 3s - loss: 0.1707 - mean_squared_error: 0.1707
Epoch 9/10
 - 3s - loss: 0.1694 - mean_squared_error: 0.1694
Epoch 10/10
 - 3s - loss: 0.1674 - mean_squared_error: 0.1674
[2018-07-23 14:19:31.603432] INFO: bigquant: cached.v2 运行完成[39.710112s].
In [15]:
# 5. LSTM 预测
dd = [None, None]
def lstm_predict(model_ds, data_ds, activation):
    import keras
    from keras.models import load_model

    keras.activations.activation_atan = activation
    try:
        model = load_model(model_ds.open_temp_path())
    except:
        model_ds.close_temp_path()
        raise

    df = data_ds.read_df()
    predictions = model.predict(
        [np.array(df['X'].values.tolist()), np.array(df['X_aux'].values)])
    df['score'] = predictions.flatten()

    # 预测值和真实值的分布
    T.plot(
        df,
        x='Y', y=['score'], chart_type='scatter',
        title='LSTM预测结果:实际值 vs. 预测值'
    )

    return Outputs(data=DataSource.write_df(df[['date', 'score']]))

lstm_m5 = M.cached.v2(run=lstm_predict, kwargs=dict(
    model_ds=lstm_m4.data,
    data_ds=lstm_m3_evaluation.data,
    activation=activation_atan
))
[2018-07-23 14:20:53.810963] INFO: bigquant: cached.v2 开始运行..
[2018-07-23 14:21:00.509760] INFO: bigquant: cached.v2 运行完成[6.698808s].
In [16]:
# 6. 回测:在沪深300上回测
def initialize(context):
    # 系统已经设置了默认的交易手续费和滑点,要修改手续费可使用如下函数
    context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))
    # 传入预测数据和真实数据
    context.predictions = context.options['prediction_ds'].read_df()

# 回测引擎:每日数据处理函数,每天执行一次
def handle_data(context, data):
    current_dt = data.current_dt.strftime('%Y-%m-%d')
    today_df = context.predictions[context.predictions.date == current_dt]
    if len(today_df) <= 0:
        return

    score = today_df.score.iloc[0]

    sid = context.symbol(context.options['instrument'])
    # 当前持仓
    cur_position = context.portfolio.positions[sid].amount
    if cur_position == 0:
        # 如果当前没有仓位
        if score > 0:
            # 如果预测要上涨
            context.order_target_percent(sid, 0.9)
            context.extension['last_buy_date'] = current_dt
    else:
        # 如果预测要下跌,并且持有超过了五天
        if score < 0:
            hold_days = context.trading_calendar.session_distance(
                pd.Timestamp(context.extension['last_buy_date']),
                pd.Timestamp(current_dt)
            )
            if hold_days >= 5:
                context.order_target(sid, 0)

# 调用回测引擎
lstm_m6 = M.trade.v2(
    instruments=[lstm_conf.instrument],
    start_date=lstm_conf.split_date,
    end_date=lstm_conf.end_date,
    initialize=initialize,
    handle_data=handle_data,
    order_price_field_buy='open',       # 表示 开盘 时买入
    order_price_field_sell='close',     # 表示 收盘 前卖出
    capital_base=10000,
    benchmark='000300.SHA',
    options={'instrument': lstm_conf.instrument, 'prediction_ds': lstm_m5.data}
)
[2018-07-23 14:21:03.520252] INFO: bigquant: backtest.v7 开始运行..
[2018-07-23 14:21:03.525966] INFO: bigquant: biglearning backtest:V7.1.2
[2018-07-23 14:21:04.962859] INFO: algo: TradingAlgorithm V1.2.1
[2018-07-23 14:21:07.048632] INFO: Performance: Simulated 565 trading days out of 565.
[2018-07-23 14:21:07.055644] INFO: Performance: first open: 2015-01-05 09:30:00+00:00
[2018-07-23 14:21:07.056834] INFO: Performance: last close: 2017-04-28 15:00:00+00:00
  • 收益率24.43%
  • 年化收益率10.24%
  • 基准收益率-2.66%
  • 阿尔法0.1
  • 贝塔0.74
  • 夏普比率0.4
  • 胜率0.62
  • 盈亏比1.0
  • 收益波动率24.35%
  • 信息比率0.05
  • 最大回撤26.03%
[2018-07-23 14:21:09.206581] INFO: bigquant: backtest.v7 运行完成[5.686289s].

期货标记数据及卖空
(kw) #2

666学习了,谢谢分享


(yuanj) #3

后面是怎么LSTM与stockranker配合的?


(胖大帅) #5

在训练的时候标记label当然要用未来的return了。只需要保证在预测阶段的输入不包括未来函数就可以了


(胖大帅) #7

https://bigquant.com/docs/data_features.html#id13
因子库中的技术分析因子应该能满足你的需求。
另外 python的talib库也可以很方便计算各种指标,这个帖子分享了talib的函数手册
https://community.bigquant.com/t/Word哥厉害了-talib函数手册-汉化标注版/96?u=somebody


(Miles) #9

请问有完整代码吗?比如github连接。谢谢!


(Miles) #10

请问能否将输入LSTM之前的 train_xtrain_y, train_auxtest_x, test_y, test_auxshape 分别告知一下,谢谢!


(Arthas) #11

您可以克隆并运行一下代码,然后用 train_x.shape 就可以看了:

  • train_xtest_xshape(None, 30, 6),其中 None 表示样本数量
  • train_auxtest_auxtrain_y, test_y 都是长为样本数量的向量

(Miles) #12

谢谢!train_aux,test_aux,train_y, test_y的长就是30是吗?

我用的train_x, test_x的shape是 (None, 15, 11),报错:

ValueError: Error when checking input: expected lstm_input to have 3 dimensions, but got array with shape (15, 11)

不知道什么原因?


(Arthas) #13

train_x中每个样本是一个30×6的二维数组,也就是说每个样本是一个时间步长为30的序列,每个时间步上又有6个features。

在本例中,train_x共有约2500的样本,所以train_x其实是三维的,它的shape是(2500,30,6)。keras的模型的input_shape只需要规定每个样本的shape,所以在‘lstm_input = Input(shape=(30,6), name=‘lstm_input’)’这里shape规定成(30,6)就可以了。

ValueError: Error when checking input: expected lstm_input to have 3 dimensions, but got array with shape (15, 11) 

是不是因为你的train_x只有两维?


(Miles) #14

非常感谢 @nsc ! 因为初学,所以还有几个问题请教,非常感谢!

  1. 那 train_y是否和train_x一样也是三维的,它的shape是(2500, 30, 6)?它的维数应该哪里修改?
  2. train_aux是二维的还是三维的,它的shape是多少? 若是二维的,可不可以是三维呢?若是,需要在哪里修改?
  3. 数据导入及初步处理中的data = D.history_data(conf.instrument, conf.start_date, conf.end_date, conf.fields)里的D对象是哪里来的。

(Arthas) #15

不客气
以下样本个数均默认2500

  1. LSTM指一种监督学习算法,监督学习中的x指一些特征,特征的数量和维度根据算法的不同而不同,如RNN(LSTM是RNN的一种)允许输入的单个样本的的维度为向量(长为A)或二维数组(长A宽B),那么x的shape就是(2500,A)或(2500,A, B)。监督学习中的 y 指标签,在回归问题中也就是我们希望用特征去拟合的值。在本例中我们希望通过一系列特征去拟合未来n日收益率,所以本例中每个样本中的y是一个值(收益率),train_y是长为2500的向量。里面的每个值对应于train_x的一个样本。
  2. 在本例中 train_aux是一个长为2500的向量,也就是每个样本的train_aux是一个值。上述模型简单表示(以单个样本为例)是:一个train_x(30,6)样本经过LSTM内一堆参数后变成了一个长为128的向量A,然后再加上一个train_aux(一个值)成为长129的向量B,B经过Dense层一堆参数之后变成1个值h,h与该样本的真是标签y比较,相差大的话,说明LSTM和Dense层里面的参数不准,则调整这些参数。
  3. D.history_data中的D对象是BQ平台的内置接口,D接口专门用来获取数据,如D.instruments用来获取一段时间内的股票代码,D.history_data用来获取一些股票的历史数据,参见文档教程

train_x,train_aux,train_y都是可更改的。帖子代码中这三者shape的定义都在数据处理那里,可以在那里进行修改。
感觉你对LSTM不是很了解,如果你想深入了解这方面的话,个人建议是:如果你英文不错的话可以读一些LSTM相关的文章,不管他们做多高深的东西,在前面都会说一说LSTM的基础知识;keras中文文档tensorflow官网上对每个API接口的介绍可以读一遍,首推keras中文文档。 多读多用就能“悟”出这东西怎么用了。
不推荐网上搜到的介绍LSTM的帖子,这些帖子讨论的东西初学者不会关心,而且绝大多数都是说那么一两个问题的,怎么说呢,天下文章一大抄,呵呵。


(Miles) #16

太谢谢了!说的很清楚。只是关于如何修改train_x,train_aux,train_y还不是很清楚。比如说我的train_aux的shape是(2500,10,1)或者train_y的shape是(2500,15,1)可以吗?要在keras哪里修改啊?谢谢!


(Arthas) #17

在模型中根据训练数据调整模型shape

lstm_input = Input(shape=(30,6), name='lstm_input')

这里的shape是train_x中单个样本的shape。若train_x的shape为(2500,A,B),则这里shape为(A,B)


aux_input = Input(shape=(1,), name='aux_input')

这里的shape是train_aux中单个样本的shape。若train_aux的shape为(2500,C,D),则这里shape为(C,D);本示例代码中单个train_aux为单个值,所以shape=(1,)


predictions = Dense(1, activation=atan)(Dense_output_2)

train_y一般是这样的形式:(2500,E),很少有三维形式,所以可以使

predictions = Dense(E, activation=atan)(Dense_output_2)

Dense接口目前不支持shape=(E,F)的情况,所以如果用Dense层的话train_y不能是(2500,E,F)。如果最后一维为1,像你说的这种情况,1就可以不要了,train_y就是(2500,15),E=15


(Miles) #18

请问:

  1. 这里的(C,D)有什么约束条件呢?因为之前lstm_output的shape是(None, 128)。是不是D只能为1?
  2. 另外,predictions还是报如下错误呢?(E=15)
    ValueError: Error when checking target: expected dense_3 to have 2 dimensions, but got array with shape (9, 15, 1)

(Arthas) #19

是的,如果train_aux是(2500,C,D)这种形式的话,没办法和lstm_output concat。一种解决办法是在aux_input后加Flatten层:Flatten层用来将输入“压平”,即把多维的输入一维化。这样就可以把train_aux转化成(2500,C*D)形式了。

aux_input = Input(shape=(C,D), name='aux_input')
flatten_data = keras.layers.core.Flatten()(aux_input)
merged_data = merge([lstm_output, flatten_data], mode='concat', concat_axis=-1)
Dense_output_1 = Dense(64, activation='linear')(merged_data)

话说回来,既然你train_aux规定成了3维,那么后面一般需要先被LSTM或其他算法处理一下再和lstm_output连,这样才能表现出”train_aux规定成了3维“的效果,否则还不如在数据处理的时候就直接把train_aux变成(2500,F)的二维shape,这样也不必加Flatten层了。


(Arthas) #20

可以把你构建的模型代码发一下吗?以及各个输入量的shape?


(liupanpan123456) #21

请问label(np.round(close/500))这个指标怎么理解