スウィーピング モンク チュートリアルの複数株運用の定期的なリバランスのケースは、複数要素の銘柄選択戦略であり、バックトレーダーが定期的に銘柄を選択し、各銘柄のウェイトを決定します (均等ウェイト法を使用)。
もう 1 つのアイデアは、バックトレーダーの外部で銘柄選択とウェイト決定のロジックを実行して、次のようなリバランス テーブルを作成することです。これは、毎月リバランス (再調整) を行い、選択した各銘柄のウェイトを与えます。
次に、リバランス フォームを Backtrader に渡し、Backtrader にリバランス フォームの情報を読み取らせ、戦略のバックテストを実行します。リバランステーブルに保存される銘柄選択結果は、実際には、各リバランス日にどの銘柄を保持すべきか、および対応するポジションウェイトです。
戦略のソース コードを以下に示します。中心となるアイデアは次のとおりです。
- __init__() でリバランス テーブルを一度に読み取り、リバランス テーブルからリバランス日付を抽出します。
- next() で、現在のバックテスト時点がリバランス日であるかどうかを判断します。リバランス日の場合は、削除されたターゲットのポジションを閉じ、新しく追加されたターゲットを購入します。非リバランス日の場合は、トリガーしません。注文操作。
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)