以下策略来自我们编写的教程和视频课程。
多股组合操作,是一种高级的操作模式。多股组合操作通常有两种模式,一种是定期调仓,即定期再平衡,比如每周1调仓,或每月1号调仓等。另一种是非定期调仓,比如每日判断,进行调仓,或者每隔n天进行调仓。
定期调仓(再平衡)通常通过定时器timer来进入调仓逻辑,而非定期调仓通常通过传统的策略next方法进入调仓逻辑。当然,这并非绝对。
这两种多股调仓操作,都不能用backtrader内置的自动确定最小期的方法来做(比如为了求20日均线,自动跳过前20个bar),因为有些股票有交易的日期很靠后,它的最小期很大,其他股票也会采用这个最小期,这会导致其他股票浪费最小期前的数据,因此,必须自己控制最小期,也就是prenext方法里必须写上self.next()直接跳转到next。然后如果用到了比如5日均线这样的指标,你要自己判断数据对象线长度是否够长。下面的案例策略没有用到此类技术指标,因此没有判断线长,理论上从第一根bar就可执行逻辑。
尽管网上有一个用backtrader执行多股组合回测的案例,但并未很好地处理好多股回测中的一些问题。本文将给出完善的处理方案。
(基于next的非定期再平衡的策略实现请参考我们编写的教程和视频课程)
本文介绍基于定时器timer的多股定期再平衡策略的实现.
本案例的目的是介绍使用backtrader进行组合管理时,要注意的一些技术要点,策略本身仅供参考。策略的大致逻辑如下:每年5月1日,9月1日,11月1日进行组合再平衡操作(若该日休市,则顺延到开市日进行再平衡操作)。
首先加载一组股票(股票池),在再平衡日,从股票池挑出至少上市3年,且净资产收益率roe>0.1,市盈率 pe在0到100间的股票,这组选出的股票再按成交量从大到小排序,选出前100只股票(如果选出的股票少于100只,则按实际来),将全部账户价值按等比例分配买入这些股票。
该策略反应了如下几个技术要点,把这些要点整明白,基本上就可用于实战了,代码更详细的解读特别是定时器timer的用法参考我们编写的教程和视频课程:
1 扩展PandasData类
2 第一个数据应该对应指数,作为时间基准
3 数据预处理:删除原始数据中无交易的及缺指标的记录
4 先平仓再执行后续买卖
5 下单量的计算方法
6 如何保证先卖后买以空出资金
7 怎样按明日开盘价计算下单数量
8 为行情数据对象提供名字
9 买卖数量如何设为100的整数倍
10 设置符合中国股市的佣金模式,考虑印花税
11 涨跌停板的处理
# 考虑中国佣金,下单量100的整数倍,涨跌停板,滑点 from datetime import datetime, time from datetime import timedelta import sys # 发现脚本名字(in argv[0]) from backtrader.feeds import PandasData # 用于扩展DataFeed class PandasDataExtend(PandasData): lines = ('pe', 'roe', 'marketdays') ('marketdays', 17), ) # 上市天数 class stampDutyCommissionScheme(bt.CommInfoBase): 本佣金模式下,买入股票仅支付佣金,卖出股票支付佣金和印花税. ('stamp_duty', 0.005), # 印花税率 ('commission', 0.001), # 佣金率 ('commtype', bt.CommInfoBase.COMM_PERC), def _getcommission(self, size, price, pseudoexec): If size is greater than 0, this indicates a long / buying of shares. If size is less than 0, it idicates a short / selling of shares. return size * price * self.p.commission elif size < 0: # 卖出,考虑印花税 return size * price * (self.p.stamp_duty + self.p.commission) return 0 # just in case for some reason the size is 0. class Strategy(bt.Strategy): rebal_monthday=[1], # 每月1日执行再平衡 num_volume=100, # 成交量取前100名 def log(self, txt, dt=None): dt = dt or self.data0.datetime.date(0) print('%s, %s' % (dt.isoformat(), txt)) self.lastRanks = [] # 上次交易股票的列表 # 0号是指数,不进入选股池,从1号往后进入股票池 self.stocks = self.datas[1:] # 记录以往订单,在再平衡日要全部取消未成交的订单 self.sma={d:bt.ind.SMA(d,period=self.p.period) for d in self.stocks} when= bt.Timer.SESSION_START, monthdays=self.p.rebal_monthday, # 每月1号触发再平衡 monthcarry=True, # 若再平衡日不是交易日,则顺延触发notify_timer def notify_timer(self, timer, when, *args, **kwargs): if self.data0.datetime.date(0).month in [5,9,11]: self.rebalance_portfolio() # 执行再平衡 # print('next 账户总值', self.data0.datetime.datetime(0), self.broker.getvalue()) # if(self.getposition(d).size!=0): # print(d._name, '持仓' ,self.getposition(d).size) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: # 订单状态 submitted/accepted,无动作 if order.status in [order.Completed]: self.log('买单执行,%s, %.2f, %i' % (order.data._name, order.executed.price, order.executed.size)) self.log('卖单执行, %s, %.2f, %i' % (order.data._name, order.executed.price, order.executed.size)) self.log('订单作废 %s, %s, isbuy=%i, size %i, open price %.2f' % (order.data._name, order.getstatusname(), order.isbuy(), order.created.size, order.data.open[0])) def notify_trade(self, trade): print('毛收益 %0.2f, 扣佣后收益 % 0.2f, 佣金 %.2f, 市值 %.2f, 现金 %.2f' % (trade.pnl, trade.pnlcomm, trade.commission, self.broker.getvalue(), self.broker.getcash())) def rebalance_portfolio(self): self.currDate = self.data0.datetime.date(0) print('rebalance_portfolio currDate', self.currDate, len(self.stocks)) # 如果是指数的最后一本bar,则退出,防止取下一日开盘价越界错 if len(self.datas[0]) == self.data0.buflen(): for o in self.order_list: self.order_list = [] # 重置订单列表 # print('sma', d._name, self.sma[d][0],self.sma[d][1], d.marketdays[0]) self.ranks = [d for d in self.stocks if len(d) > 0 # 重要,到今日至少要有一根实际bar and d.marketdays > 3*365 # 到今天至少上市 # 今日未停牌 (若去掉此句,则今日停牌的也可能进入,并下订单,次日若复牌,则次日可能成交)(假设原始数据中已删除无交易的记录) and d.datetime.date(0) == self.currDate and len(d) >= self.p.period and d.close[0] > self.sma[d][1] self.ranks.sort(key=lambda d: d.volume, reverse=True) # 按成交量从大到小排序 self.ranks = self.ranks[0:self.p.num_volume] # 取前num_volume名 if len(self.ranks) == 0: # 无股票选中,则返回 data_toclose = set(self.lastRanks) - set(self.ranks) print('sell 平仓', d._name, self.getposition(d).size) self.order_list.append(o) # 记录订单 # 每只股票买入资金百分比,预留2%的资金以应付佣金和计算误差 buypercentage = (1-0.02)/len(self.ranks) targetvalue = buypercentage * self.broker.getvalue() self.ranks.sort(key=lambda d: self.broker.getvalue([d]), reverse=True) self.log('下单, 标的个数 %i, targetvalue %.2f, 当前总市值 %.2f' % (len(self.ranks), targetvalue, self.broker.getvalue())) # 按次日开盘价计算下单量,下单量是100的整数倍 abs((self.broker.getvalue([d]) - targetvalue) / d.open[1] // 100 * 100)) validday = d.datetime.datetime(1) # 该股下一实际交易日 if self.broker.getvalue([d]) > targetvalue: # 持仓过多,要卖 lowerprice = d.close[0]*0.9+0.02 o = self.sell(data=d, size=size, exectype=bt.Order.Limit, price=lowerprice, valid=validday) upperprice = d.close[0]*1.1-0.02 o = self.buy(data=d, size=size, exectype=bt.Order.Limit, price=upperprice, valid=validday) self.order_list.append(o) # 记录订单 self.lastRanks = self.ranks # 跟踪上次买入的标的 ########################## ######################### cerebro = bt.Cerebro(stdstats=False) cerebro.addobserver(bt.observers.Broker) cerebro.addobserver(bt.observers.Trades) # cerebro.broker.set_coc(True) # 以订单创建日的收盘价成交 # cerebro.broker.set_coo(True) # 以次日开盘价成交 datadir = './dataswind' # 数据文件位于本脚本所在目录的data子目录中 datafilelist = glob.glob(os.path.join(datadir, '*')) # 数据文件路径列表 maxstocknum = 20 # 股票池最大股票数目 # 注意,排序第一个文件必须是指数数据,作为时间基准 datafilelist = datafilelist[0:maxstocknum] # 截取指定数量的股票池 for fname in datafilelist: # df = df[~df['交易状态'].isin(['停牌一天'])] # 去掉停牌日记录 df['date'] = pd.to_datetime(df['date']) # 转成日期类型 openinterest=-1, # 无未平仓量列 fromdate=datetime(2002, 4, 1), # 起始日2002, 4, 1 todate=datetime(2015, 12, 31), # 结束日 2015, 12, 31 ticker = fname[-13:-4] # 从文件路径名取得股票代码 cerebro.adddata(data, name=ticker) cerebro.addstrategy(Strategy) cerebro.broker.setcash(startcash) # 防止下单时现金不够被拒绝。只在执行时检查现金够不够。 cerebro.broker.set_checksubmit(False) comminfo = stampDutyCommissionScheme(stamp_duty=0.001, commission=0.001) cerebro.broker.addcommissioninfo(comminfo) print('最终市值: %.2f' % cerebro.broker.getvalue())
以上这个策略,其实也可以通过next方法进入策略逻辑,具体代码和详情请参考我们的教程。
作者:扫地僧2020
|