EEMD-LSTM模型择时策略 --- 2. 量化回测

我们已经得到了一个使用前七天的数据预测下一天收盘价数据的模型, 由于模型预测精度较高, 这里我们将使用较为简单的量化策略:

  1. 使用今天加上前六天的开盘与收盘价数据预测下一天的收盘价
  2. 如果下一天的收盘价高于今天的收盘价, 就买入全部资产90%的股票
  3. 如果下一天的收盘价低于今天的收盘价, 就卖出全部股票

资料来源

该方法将于量化中最为常见的金叉死叉策略进行比较
总资产假定为100000元, 手续费为0.03%, 回测框架使用backtrader

一. 数据与模型导入

! pip install -i https://mirrors.aliyun.com/pypi/simple/ backtrader
! pip install -i https://mirrors.aliyun.com/pypi/simple/ EMD-signal

在这里插入图片描述

!pip install -i https://mirrors.aliyun.com/pypi/simple/ git+https://github.com/quantopian/pyfolio 
#导入各种库
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from pylab import mpl
from PyEMD import EEMD
import datetime
import backtrader as bt
import pickle
import torch
import torch.nn as nn
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import StandardScaler
# 超参数
input_size = 14
hidden_size = 50
num_layers = 2
output_size = 1
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 定义LSTM模型
class LSTMModel(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size):
        super(LSTMModel, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
        c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
        out, _ = self.lstm(x, (h0, c0))
        out = self.fc(out[:, -1, :])
        return out
# 创建一个空列表,用于存储加载的模型
loaded_models = []

# 加载六个模型
for i in range(1, 7):
    model_path = f'lstm_model{i}.pth'
    loaded_model = LSTMModel(input_size, hidden_size, num_layers, output_size).to(device)
    loaded_model.load_state_dict(torch.load(model_path))
    loaded_model.eval()
    loaded_models.append(loaded_model)
df = pd.read_csv("/home/mw/input/stock9243/data_00300.csv")
df['date'] = pd.to_datetime(df['date'],format='%Y-%m-%d %H:%M:%S%z')
df['date'] = df['date'].dt.date
df['date'] = pd.to_datetime(df['date'])
df = df.set_index('date')
df = df[['open', 'high', 'low', 'close', 'volume']]
df.index[2917]

在这里插入图片描述

二. 金叉死叉策略

最经典的量化策略

  1. 金叉:短期均线上穿长期均线, 这时买入总资产90%的股票
  2. 死叉:短期均线下穿长期均线, 这时卖出所有股票

此处短期均线为5日均线, 长期均线为20均线

class SMA(bt.Strategy):
    def log(self, txt, dt=None):
        '''此策略的日志记录功能'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))
        
    def __init__(self):
        self.dataclose = self.datas[0].close
        self.order = None
        self.buyprice = None
        self.buycomm = None

        self.sma_short = bt.indicators.SimpleMovingAverage(self.data0, period=5)  # 短期均线
        self.sma_long = bt.indicators.SimpleMovingAverage(self.data0, period=20)  # 长期均线

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # 做多/做空 订单 已提交/已执行 到/被代理 - 无事可做
            return

        # 检查订单是否已经完成
        # 注意:如果没有足够资金,代理可能拒绝订单
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order.executed.price,
                     order.executed.value,
                     order.executed.comm))

                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            else:  # 做空
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))

            self.bar_executed = len(self)

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')

        self.order = None

    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
                 (trade.pnl, trade.pnlcomm))

        
    def next(self):
        if self.order:
            return
        if not self.position:
            # 金叉:短期均线上穿长期均线
            if self.sma_short[0] > self.sma_long[0] and self.sma_short[-1] < self.sma_long[-1]:
                self.log('BUY CREATE, %.2f' % self.dataclose[0])
                self.buy()
        else:
            # 死叉:短期均线下穿长期均线
            if self.sma_short[0] < self.sma_long[0] and self.sma_short[-1] > self.sma_long[-1]:
                self.log('SELL CREATE, %.2f' % self.dataclose[0])
                self.sell()
if __name__ == '__main__':
    cerebro = bt.Cerebro()
    data = bt.feeds.PandasData(dataname = df, 
                               fromdate = df.index[2917], 
                               todate = df.index[-1], 
                               timeframe=bt.TimeFrame.Days)
    cerebro.adddata(data)
    # cerebro.adddata(data_pingan)
    cerebro.addstrategy(SMA)
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name = 'SharpeRatio')
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name = 'DrawDown')

    cerebro.broker.setcash(100000.0)
    cerebro.broker.setcommission(commission = 0.0003)

    cerebro.addsizer(bt.sizers.PercentSizer, percents=90)
    cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')  
    # 打印起始条件
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
    result = cerebro.run()


    # 打印最终结果
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

在这里插入图片描述

# 获取分析器结果
sharpe_ratio = result[0].analyzers.SharpeRatio.get_analysis()
max_drawdown = result[0].analyzers.DrawDown.get_analysis()

# 打印夏普比率、最大回撤
print('Sharpe Ratio:', sharpe_ratio['sharperatio'])
print('Max Drawdown:', max_drawdown['max']['drawdown'])

在这里插入图片描述

# 计算总收益率
total_return = cerebro.broker.getvalue() / cerebro.broker.startingcash - 1

# 计算交易天数
start_date = df.index[2917]
end_date = df.index[-1]
trading_days = (end_date - start_date).days

# 计算年化收益率
annual_return = (1 + total_return) ** (365.0 / trading_days) - 1

print("Total Return:", total_return)
print("Annualized Return:", annual_return)

在这里插入图片描述

import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
strat = result[0]
pyfoliozer = strat.analyzers.getbyname('pyfolio')
returns, positions, transactions, gross_lev = pyfoliozer.get_pf_items()
import pyfolio as pf
pf.create_full_tear_sheet(
    returns,
    positions=positions,
    transactions=transactions,
    live_start_date='2022-01-05')
cerebro.plot(iplot=False)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
金叉死叉策略的表现结果很差,夏普比率为-0.508, 最大回撤为16.98, 回撤还可以, 总收益率-7.25%, 年化收益率-3.19%
100000元的本金经过两年的买卖变为92749.16, 净亏7250.83
从图中也可以看出红点多于蓝点, 意味着大多是亏钱的交易, 胜率很低
由于2022年至2024年沪深300整体下跌, 金叉死叉策略失效也能理解

三. EEMD-LSTM策略

# 初始化 EEMD 对象
eemd = EEMD()

# 执行 EEMD 分解
eIMFs_open = eemd.eemd(df['open'].values, max_imf=5)
eIMFs_close = eemd.eemd(df['close'].values, max_imf=5)
class SMA(bt.Strategy):
    def log(self, txt, dt=None):
        '''此策略的日志记录功能'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))
        
    def __init__(self):
        self.dataclose = self.datas[0].close
        self.dataopen = self.datas[0].open
        self.prediction_data = []  # 存储前七天的开盘价和收盘价数据
        self.totaldata = df[['open','close']][:2917]
        self.order = None
        self.buyprice = None
        self.buycomm = None

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # 做多/做空 订单 已提交/已执行 到/被代理 - 无事可做
            return

        # 检查订单是否已经完成
        # 注意:如果没有足够资金,代理可能拒绝订单
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order.executed.price,
                     order.executed.value,
                     order.executed.comm))

                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            else:  # 做空
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))

            self.bar_executed = len(self)

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')

        self.order = None

    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
                 (trade.pnl, trade.pnlcomm))

    def next(self):
        new_data={
            'open': self.dataopen[0],
            'close': self.dataclose[0],
        }
        new_row = pd.DataFrame(new_data, index=[0])  # 将字典转换为 DataFrame,并设置索引
        self.totaldata = pd.concat([self.totaldata, new_row], ignore_index=True)
        if len(self.prediction_data) < 7:
            # 如果数据不足七天,则继续收集数据
            self.prediction_data.append((self.dataopen[0], self.dataclose[0]))
            return
        if self.order:
            return

        lstm_re = []
        for i in range(6):
            window_size = 7  # 滞后阶数为7天
            n_steps = 1      # 每次预测1个时间步
            
            df_ = pd.DataFrame(np.concatenate([eIMFs_close[i].reshape(-1, 1), eIMFs_open[i].reshape(-1, 1)], axis=1), columns=['close', 'open'])
            
            # 生成窗口数据
            windowed_data = [df_.iloc[i - window_size:i, [0, 1]].values.flatten() for i in range(window_size, len(df_))]
            
            new_df = pd.DataFrame(windowed_data, columns=[f'x{i}' for i in range(1, 2 * window_size + 1)])
            new_df['target'] = df_.iloc[window_size:, 0].values
            
            # 创建特征矩阵X和目标变量y
            X = np.array([new_df.iloc[i:i + n_steps, :-1].values for i in range(len(new_df) - n_steps + 1)])
            y = new_df['target']
            
            # 数据标准化
            scaler_X = StandardScaler()
            scaler_y = StandardScaler()
            X_reshaped = X.reshape(X.shape[0] * X.shape[1], X.shape[2])
            X_scaled = scaler_X.fit_transform(X_reshaped)
            X_scaled_reshaped = X_scaled.reshape(X.shape[0], X.shape[1], X.shape[2])
            y_scaled = scaler_y.fit_transform(y.values.reshape(-1, 1))
            pre_x = X_scaled_reshaped[len(self.totaldata)-window_size:len(self.totaldata)-window_size+1]
            
            with torch.no_grad():
                y_pred = loaded_models[i](torch.tensor(pre_x, dtype=torch.float32).to(device)).cpu().numpy().flatten()
            # 最后一个由于没有数据直接return
            if y_pred.size > 0:
                y_pred = scaler_y.inverse_transform(y_pred.reshape(-1, 1)).flatten()
            else:
                return
            lstm_re.append(y_pred)

        # 将六个模型的预测结果求和得下一天的收盘价预测值
        next_close_prediction = sum(lstm_re) 

        if not self.position:
            # 如果下一天的收盘价预测值大于今天的收盘价: 买
            if next_close_prediction > self.dataclose[0]:
                self.log('BUY CREATE, %.2f' % self.dataclose[0])
                self.buy()
        else:
            # 如果下一天的收盘价预测值小于今天的收盘价: 卖
            if next_close_prediction < self.dataclose[0]:
                self.log('SELL CREATE, %.2f' % self.dataclose[0])
                self.sell()


# 由于每天都要使用六个模型进行预测, 运行时间可能较慢, 大概需要30-40mins
if __name__ == '__main__':
    cerebro = bt.Cerebro()
    data = bt.feeds.PandasData(dataname = df, 
                               fromdate = df.index[2917], 
                               todate = df.index[-1], 
                               timeframe=bt.TimeFrame.Days)

    cerebro.adddata(data)
    cerebro.addstrategy(SMA)
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name = 'SharpeRatio')
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name = 'DrawDown')

    cerebro.broker.setcash(100000.0)
    cerebro.broker.setcommission(commission = 0.0003)

    cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')  
    cerebro.addsizer(bt.sizers.PercentSizer, percents=90)
    # 打印起始条件
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
    result = cerebro.run()
    

    # 打印最终结果
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

在这里插入图片描述

    # 保存 cerebro 对象和结果
with open('EEMD_LSTM策略.pkl', 'wb') as f:
    pickle.dump((cerebro, result), f)

print('Cerebro and result saved successfully.')

在这里插入图片描述

# 获取分析器结果
sharpe_ratio = result[0].analyzers.SharpeRatio.get_analysis()
max_drawdown = result[0].analyzers.DrawDown.get_analysis()

# 打印夏普比率、最大回撤
print('Sharpe Ratio:', sharpe_ratio['sharperatio'])
print('Max Drawdown:', max_drawdown['max']['drawdown'])

在这里插入图片描述

# 计算总收益率
total_return = cerebro.broker.getvalue() / cerebro.broker.startingcash - 1

# 计算交易天数
start_date = df.index[2917]
end_date = df.index[-1]
trading_days = (end_date - start_date).days

# 计算年化收益率
annual_return = (1 + total_return) ** (365.0 / trading_days) - 1

print("Total Return:", total_return)
print("Annualized Return:", annual_return)

在这里插入图片描述

import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
strat = result[0]
pyfoliozer = strat.analyzers.getbyname('pyfolio')
returns, positions, transactions, gross_lev = pyfoliozer.get_pf_items()
import pyfolio as pf
pf.create_full_tear_sheet(
    returns,
    positions=positions,
    transactions=transactions,
    live_start_date='2022-01-05')
cerebro.plot(iplot=False)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
EEMD-LSTM策略的表现姣好, 夏普比率2.54, 最大回撤2.75, 总收益率194%, 年化收益率59%
本金由100000元经过两年交易变为 297425.23
相比之下金叉死叉策略甚至做不到稳定盈利
此外, 由于该策略买卖的唯一条件就是下一天预测的收盘价与当天收盘价的对比, 所有该策略会较为频繁的交易, 以求获得更多的超额利润
图中也可以看出觉大多是都是蓝色点, 代表是成功的交易, 胜率很高

PS: 由于LSTM模型的神经元权重具有随机性, 每一次运行的结果会在一个合理范围内波动

if __name__ == '__main__':
    # 加载 cerebro 对象和结果
    with open('EEMD_LSTM策略.pkl', 'rb') as f:
        cerebro, result = pickle.load(f)

    # 可以继续使用这些对象
    print('Cerebro and result loaded successfully.')

资料来源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值