【干货】用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')