backtrader strategy library: multi-factor stock selection strategy based on bt external transfer position table

The case of periodic rebalancing of multi-share operations in the Sweeping Monk tutorial is a multi-factor stock selection strategy. It is within the backtrader to regularly select stocks and determine the weight of each stock (using the equal weight method).

Another idea is to do the logic of stock selection and weight determination outside the backtrader to form a rebalancing table similar to the following. This table rebalancing (rebalancing) on ​​a monthly basis and gives the weight of each selected stock:

Then pass the rebalancing form to Backtrader, let Backtrader read the information on the rebalancing form, and carry out strategy backtesting. The stock selection results stored on the rebalancing table are actually which stocks should be held on each rebalancing day and the corresponding position weights.

The source code of the strategy is given below, the core idea is:

  • Read the rebalancing table in __init__() at one time, and extract the rebalancing date from the rebalancing table;
  • In next(), determine whether the current backtest time point is a rebalancing day: if it is a rebalancing day, close the position of the excluded target and buy the newly added target; if it is a non-rebalancing day, do not trigger the order operation .

 

import backtrader as bt
import pandas as pd
import datetime

# 回测策略
class StockSelectStrategy(bt.Strategy):
    '''多因子选股 - 基于调仓表'''
    def __init__(self):
        # 读取调仓表,表结构如下所示:
        #       trade_date  sec_code    weight
        # 0     2019-01-31  000006.SZ   0.007282
        # 1     2019-01-31  000008.SZ   0.009783
        # ...   ...         ...         ...
        # 2494  2021-01-28  688088.SH   0.007600
        self.buy_stock = pd.read_csv("./data/trade_info.csv", parse_dates=['trade_date']) 
        # 读取调仓日期,即每月的最后一个交易日,回测时,会在这一天下单,然后在下一个交易日,以开盘价买入
        self.trade_dates = pd.to_datetime(self.buy_stock['trade_date'].unique()).tolist()
        self.order_list = []  # 记录以往订单,方便调仓日对未完成订单做处理
        self.buy_stocks_pre = [] # 记录上一期持仓
    

    def log(self, txt, dt=None):
        ''' 策略日志打印函数'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def next(self):
        dt = self.datas[0].datetime.date(0) # 获取当前的回测时间点
        # 如果是调仓日,则进行调仓操作
        if dt in self.trade_dates:
            print("--------------{} 为调仓日----------".format(dt))
            # 在调仓之前,取消之前所下的没成交也未到期的订单
            if len(self.order_list) > 0:
                for od in self.order_list:
                    self.cancel(od) # 如果订单未完成,则撤销订单
                self.order_list = []  #重置订单列表
            # 提取当前调仓日的持仓列表
            buy_stocks_data = self.buy_stock.query(f"trade_date=='{dt}'")
            long_list = buy_stocks_data['sec_code'].tolist()
            print('long_list', long_list)  # 打印持仓列表
            # 对现有持仓中,调仓后不再继续持有的股票进行卖出平仓
            sell_stock = [i for i in self.buy_stocks_pre if i not in long_list]
            print('sell_stock', sell_stock) # 打印平仓列表
            if len(sell_stock) > 0:
                print("-----------对不再持有的股票进行平仓--------------")
                for stock in sell_stock:
                    data = self.getdatabyname(stock)
                    if self.getposition(data).size > 0 :
                        od = self.close(data=data)  
                        self.order_list.append(od) # 记录卖出订单
            # 买入此次调仓的股票:多退少补原则
            print("-----------买入此次调仓期的股票--------------")
            for stock in long_list:
                w = buy_stocks_data.query(f"sec_code=='{stock}'")['weight'].iloc[0] # 提取持仓权重
                data = self.getdatabyname(stock)
                order = self.order_target_percent(data=data, target=w*0.95) # 为减少可用资金不足的情况,留 5% 的现金做备用
                self.order_list.append(order)
       
            self.buy_stocks_pre = long_list  # 保存此次调仓的股票列表
        
    def notify_order(self, order):
        # 未被处理的订单
        if order.status in [order.Submitted, order.Accepted]:
            return
        # 已经处理的订单
        if order.status in [order.Completed, order.Canceled, order.Margin]:
            if order.isbuy():
                self.log(
                        'BUY EXECUTED, ref:%.0f, Price: %.2f, Cost: %.2f, Comm %.2f, Size: %.2f, Stock: %s' %
                        (order.ref, # 订单编号
                         order.executed.price, # 成交价
                         order.executed.value, # 成交额
                         order.executed.comm, # 佣金
                         order.executed.size, # 成交量
                         order.data._name))  # 股票名称
            else:  # Sell
                self.log('SELL EXECUTED, ref:%.0f, Price: %.2f, Cost: %.2f, Comm %.2f, Size: %.2f, Stock: %s' %
                            (order.ref,
                             order.executed.price,
                             order.executed.value,
                             order.executed.comm,
                             order.executed.size,
                             order.data._name))
        

# 实例化 cerebro
cerebro = bt.Cerebro()
# 读取行情数据 
daily_price = pd.read_csv("./data/daily_price.csv", parse_dates=['datetime'])
daily_price = daily_price.set_index(['datetime'])  # 将datetime设置成index
# 按股票代码,依次循环传入数据
for stock in daily_price['sec_code'].unique():
    # 日期对齐
    data = pd.DataFrame(index=daily_price.index.unique()) # 获取回测区间内所有交易日
    df = daily_price.query(f"sec_code=='{stock}'")[['open','high','low','close','volume','openinterest']]
    data_ = pd.merge(data, df, left_index=True, right_index=True, how='left')
    # 缺失值处理:日期对齐时会使得有些交易日的数据为空,所以需要对缺失数据进行填充
    data_.loc[:,['volume','openinterest']] = data_.loc[:,['volume','openinterest']].fillna(0)
    data_.loc[:,['open','high','low','close']] = data_.loc[:,['open','high','low','close']].fillna(method='pad')
    data_.loc[:,['open','high','low','close']] = data_.loc[:,['open','high','low','close']].fillna(0)
    # 导入数据
    datafeed = bt.feeds.PandasData(dataname=data_, 
                                   fromdate=datetime.datetime(2019,1,2), 
                                   todate=datetime.datetime(2021,1,28))
    cerebro.adddata(datafeed, name=stock) # 通过 name 实现数据集与股票的一一对应
    print(f"{stock} Done !") 
# 初始资金 100,000,000    
cerebro.broker.setcash(100000000.0) 
# 佣金,双边各 0.0003
cerebro.broker.setcommission(commission=0.0003) 
# 滑点:双边各 0.0001
cerebro.broker.set_slippage_perc(perc=0.0001) 
# 将编写的策略添加给大脑,别忘了 !
cerebro.addstrategy(StockSelectStrategy)
# 回测时需要添加 PyFolio 分析器
cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')
result = cerebro.run()
# 借助 pyfolio 进一步做回测结果分析

pyfolio = result[0].analyzers.pyfolio  # 注意:后面不要调用 .get_analysis() 方法
# 或者是 result[0].analyzers.getbyname('pyfolio')
returns, positions, transactions, gross_lev = pyfolio.get_pf_items()

import pyfolio as pf
pf.create_full_tear_sheet(returns)

Guess you like

Origin blog.csdn.net/qtbgo/article/details/126609746