添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
首发于 backtrader量化投资回测与交易

【干货】用backtrader做沪深300指数成份股调整策略

用backtrader做股票策略,基本上不用修改代码都可以实现。具体教程可以 参考官网 ,上面教程非常详细,比大多数的开源框架友好很多,看一遍教程,基本可以实现自己想要的策略。

首先,说下回测的逻辑和结果。

# 据悉,在每次条深300指数的成份股调整的时候,新入选的股票将会产生超额收益,被剔除的时候,将会产生负的的alpha.
# 非实战策略哈,假设使用100万资金,每次等额买入调入的股票、卖出调出的股票,持有N天后平仓,观察盈利效果。
# 多空中性策略,在对A股做空有限制的情况下,不考虑融资融券细节,假设可以做空
# 这篇文章目的仅仅是展示backtrader在股票上的使用。
年度收益率
日收益率和累计收益率
持仓数据

首先,从年度收益率和累计收益率上来看,这个策略是亏损的。在指数成份股调入后做多,指数成份股调出后做空,多空组合起来,得到的收益率是负的,虽然违反直觉,但是从策略进化的角度,很容易理解。这个策略赚钱的本质在于一些ETF基金的被动投资和信息传播的不及时,当该只股票调入沪深300指数之后,该基金要被动买入,推动股价升高;同理,调出的时候,要被动卖出,推动股价的下跌。从累计收益率上里看,在2005年5月到2007年8月,该策略却是是赚钱的。然后,2007年8月到2011年2月持续性亏损。2011年2月到2014年7月,基本上不赚不亏;2014年7月到2017年12月,开始持续性亏损,2018年1月到2019年12月期间(至今),基本上不赚不亏。这是一个很有意思的现象。非常有意思。我似乎从中看到了一点点进化的味道,正如达尔文所说的那样,在这个自然界,生存下来的,从来不是最强壮的个体,而是适应能力更强的个体。策略也一样,适应能力(稳定性)大于强壮(收益)。

其次,从持仓价值来看,很容易看出,当前的数据是有问题的。沪深300指数成份股的个数都是300个,调入和调出的个数应该一样,做多和做空的价值应该比较接近,也就是所,总的头寸应该是接近0的。所以,这是一个近似的结果。

不说了,去debug期货的回测与交易了。下面是回测代码:

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
import time 
import datetime  
import os
import sys  
import backtrader as bt
import numpy as np
import pandas as pd
import random
import plotly as py
import plotly.graph_objs as go
from backtrader.plot.plot import plot_results  #我自己编写的,运行时去掉
from backtrader.feeds import GenericCSVData
# 在交易信息之外,额外增加了PE、PB指标,做其他策略使用
class GenericCSV_PB_PE(GenericCSVData):
    # Add a 'pe' line to the inherited ones from the base class
    lines = ('factor','pe_ratio','pb_ratio',)
    # openinterest in GenericCSVData has index 7 ... add 1
    # add the parameter to the parameters inherited from the base class
    params = (('factor', 8),('pe_ratio',9),('pb_ratio',10),)
# 获取每日的沪深300的成分股的数据
import pickle
with open("/home/yun/data/index_300_stock_list.pkl",'rb') as f:
    date_stock_list=pickle.load(f)
# 编写策略
class adjust_strategy(bt.Strategy):
    params = (("hold_days",30),)
    def log(self, txt, dt=None):
        ''' Logging function fot this strategy'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))
    def __init__(self):
        # Keep a reference to the "close" line in the data[0] dataseries
        self.count=0
        self.my_hold_dict={}
        self.pre_index_300_list=[]
    def prenext(self):
    def next(self):
        # 假设有100万资金,每次成份股调整,每个股票使用1万元
        self.count+=1
        # 计算持仓现有的天数
        for key in self.my_hold_dict.keys():
            self.my_hold_dict[key]+=1
        self.log("count:{},open:{},high:{},low:{},close:{},volume:{},\
                 openinterest:{},factor:{},pb_ratio:{},pe_ratio:{}".format(
                self.count,self.datas[0].open[0],self.datas[0].high[0],
                self.datas[0].low[0],self.datas[0].close[0],self.datas[0].volume[0],
                self.datas[0].openinterest[0],self.datas[0].factor[0],
                self.datas[0].pb_ratio[0],self.datas[0].pe_ratio[0]))
        # 得到当天的时间
        current_date=self.datas[0].datetime.date(0)
        # 获得当天的交易的股票
        index_300_list=date_stock_list[str(current_date)]
        assert len(index_300_list)>0
        if current_date<=datetime.datetime(2005, 4, 8).date():
            return
        if self.count>10:
            # 如果到了持有期,就平掉这支股票
            close_list=[]
            for key in self.my_hold_dict.keys():
                if self.my_hold_dict[key]>=self.p.hold_days:
                    close_list.append(key)
            # 从持仓中删除
            for stock in close_list:
                self.my_hold_dict.pop(stock)
            for data in self.datas:
                if data._name in close_list:
                    self.close(data)
            # 计算是否有新的股票并进行开仓    
            long_list=[]
            short_list=[]
            # self.log("index_300_list:{}".format(index_300_list))
            # 新调入的股票做多
            if self.pre_index_300_list is not None:
                for stock in index_300_list:
                    if stock not in self.pre_index_300_list:
                        long_list.append(stock)
            # 新调出的股票做空
            if self.pre_index_300_list is not None:
                for stock in self.pre_index_300_list:
                    if stock not in index_300_list:
                        short_list.append(stock)
            # 循环股票,决定做多和做空
            for data in self.datas:
                if data._name in long_list:
                    close_price=data.close[0]
                    lots=int(100/close_price)*100
                    self.buy(data,size=lots)
                    self.my_hold_dict[data._name]=0
                if data._name in short_list:
                    close_price=data.close[0]
                    lots=int(100/close_price)*100
                    self.sell(data,size=lots)
                    self.my_hold_dict[data._name]=0
        # 前一日的成份股 
        self.pre_index_300_list=index_300_list
    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return
        # Check if an order has been completed
        # Attention: broker could reject order if not enougth cash
        if order.status in [order.Completed, order.Canceled, order.Margin]:
            if order.isbuy():
                self.log(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order.executed.price,
                     order.executed.value,
                     order.executed.comm))
            else:  # Sell
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))
    def notify_trade(self, trade):
        if trade.isclosed:
            self.log('TRADE PROFIT, GROSS %.2f, NET %.2f' %
                     (trade.pnl, trade.pnlcomm))
begin_time=time.time()
cerebro = bt.Cerebro()
# cerebro.broker = bt.brokers.BackBroker()  # 0.5%
# cerebro.broker.set_slippage_fixed(1, slip_open=True)
# Add a strategy
cerebro.addstrategy(adjust_strategy)
    # 优化参数
    #strats = cerebro.optstrategy(
    #    momentum_strategy,
    #    look_back_days=[5,10],
    #    hold_days=[5,10,15,20],
    #    hold_percent=[0.1,0.2])
    # Datas are in a subfolder of the samples. Need to find where the script is
    # because it could have been called from anywhere
joinquant_day_kwargs = dict(
            fromdate = datetime.datetime(2005, 4, 8),
            todate = datetime.datetime(2019, 11, 29),
            timeframe = bt.TimeFrame.Days,
            compression = 1,
            dtformat=('%Y-%m-%d'),
            datetime=0,
            high=4,
            low=3,
            open=1,
            close=2,
            volume=5,
            openinterest=6,
            factor=7,
            pb_ratio=10,
            pe_ratio=11)
data_path="/home/yun/data/stocks/index_300/day/"
file_list=list(os.listdir(data_path))
file_list=file_list
# print(file_list)
# data_path="C:/data/RB1910.csv"
count=0
for file in file_list:
    print(count,file)
    count+=1
    feed = GenericCSV_PB_PE(dataname = data_path+file, **joinquant_day_kwargs)
    cerebro.adddata(feed, name = file[:-4])
cerebro.broker.setcommission(commission=0.0005)
cerebro.broker.setcash(1000000.0)
# 添加相应的费用,杠杆率
# 获取策略运行的指标
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')
cerebro.addanalyzer(bt.analyzers.AnnualReturn, _name='_AnnualReturn')
cerebro.addanalyzer(bt.analyzers.Calmar, _name='_Calmar')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='_DrawDown')
cerebro.addanalyzer(bt.analyzers.TimeDrawDown, _name='_TimeDrawDown')
cerebro.addanalyzer(bt.analyzers.GrossLeverage, _name='_GrossLeverage')
cerebro.addanalyzer(bt.analyzers.PositionsValue, _name='_PositionsValue')
cerebro.addanalyzer(bt.analyzers.LogReturnsRolling, _name='_LogReturnsRolling')
cerebro.addanalyzer(bt.analyzers.PeriodStats, _name='_PeriodStats')
cerebro.addanalyzer(bt.analyzers.Returns, _name='_Returns')
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='_SharpeRatio')
cerebro.addanalyzer(bt.analyzers.SharpeRatio_A, _name='_SharpeRatio_A')
cerebro.addanalyzer(bt.analyzers.SQN, _name='_SQN')
cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='_TimeReturn')
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='_TradeAnalyzer')