查看账户情况:
class TestStrategy(bt.Strategy):
def next(self):
print('当前可用资金', self.broker.getcash())
print('当前总资产', self.broker.getvalue())
print('当前持仓量', self.broker.getposition(self.data).size)
print('当前持仓成本', self.broker.getposition(self.data).price)
# 也可以直接获取持仓
print('当前持仓量', self.getposition(self.data).size)
print('当前持仓成本', self.getposition(self.data).price)
# 注:getposition() 需要指定具体的标的数据集
滑点设置:
# 方式1:通过 BackBroker 类中的 slip_perc 参数设置百分比滑点
cerebro.broker = bt.brokers.BackBroker(slip_perc=0.0001)
# 方式2:通过调用 brokers 的 set_slippage_perc 方法设置百分比滑点
cerebro.broker.set_slippage_perc(perc=0.0001)
# 方式1:通过 BackBroker 类中的 slip_fixed 参数设置固定滑点
cerebro.broker = bt.brokers.BackBroker(slip_fixed=0.001)
# 方式2:通过调用 brokers 的 set_slippage_fixed 方法设置固定滑点
cerebro.broker = cerebro.broker.set_slippage_fixed(fixed=0.001)
参数说明:
有关滑点的其他设置
slip_open:是否对开盘价做滑点处理,该参数在 BackBroker() 类中默认为 False,在 set_slippage_perc 和set_slippage_fixed 方法中默认为 True;
slip_match:是否将滑点处理后的新成交价与成交当天的价格区间 low ~ high 做匹配,如果为 True,则根据新成交价重新匹配调整价格区间,确保订单能被执行;如果为 False,则不会与价格区间做匹配,订单不会执行,但会在下一日执行一个空订单;默认取值为 True;
slip_out:如果新成交价高于最高价或低于最高价,是否以超出的价格成交,如果为 True,则允许以超出的价格成交;如果为 False,实际成交价将被限制在价格区间内 low ~ high;默认取值为 False;
slip_limit:是否对限价单执行滑点,如果为 True,即使 slip_match 为Fasle,也会对价格做匹配,确保订单被执行;如果为 False,则不做价格匹配;默认取值为 True。
# 情况1:
set_slippage_fixed(fixed=0.35,
slip_open=False,
slip_match=True,
slip_out=False)
# 由于 slip_open=False ,不会对开盘价做滑点处理,所以仍然以原始开盘价 32.63307367 成交
# 情况2:
set_slippage_fixed(fixed=0.35,
slip_open=True,
slip_match=True,
slip_out=False)
# 情况3:
set_slippage_fixed(fixed=0.35,
slip_open=True,
slip_match=True,
slip_out=True)
# 滑点调整的新成交价为 32.63307367+0.35 = 32.98307367,超出了当天最高价 32.94151482
# 允许做价格匹配 slip_match=True, 而且运行以超出价格区间的新成交价执行 slip_out=True
# 最终以新成交价 32.98307367 成交
# 情况4:
set_slippage_fixed(fixed=0.35,
slip_open=True,
slip_match=False,
slip_out=True)
# 滑点调整的新成交价为 32.63307367+0.35 = 32.98307367,超出了当天最高价 32.94151482
# 由于不进行价格匹配 slip_match=False,新成交价超出价格区间无法成交
# 2019-01-17 这一天订单不会执行,但会在下一日 2019-01-18 执行一个空订单
# 再往后的 2019-07-02,也未执行订单,下一日 2019-07-03 执行空订单
# 即使 2019-07-03的 open 39.96627412+0.35 < high 42.0866713 满足成交条件,也不会补充成交
交易税费管理
股票:目前 A 股的交易费用分为 2 部分:佣金和印花税,
其中佣金双边征收,不同证券公司收取的佣金各不相同,一般在 0.02%-0.03% 左右,单笔佣金不少于 5 元;
印花税只在卖出时收取,税率为 0.1%。
期货:期货交易费用包括交易所收取手续费和期货公司收取佣金 2 部分,交易所手续费较为固定,
不同期货公司佣金不一致,而且不同期货品种的收取方式不相同,有的按照固定费用收取,有的按成交金额的固定百分比收取:
合约现价*合约乘数*手续费费率。除了交易费用外,期货交易时还需上交一定比例的保证金 。
Backtrader 也提供了多种交易费设置方式,既可以简单的通过参数进行设置,也可以结合交易条件自定义费用函数:
根据交易品种的不同,Backtrader 将交易费用分为 股票 Stock-like 模式和期货 Futures-like 种模式;
根据计算方式的不同,Backtrader 将交易费用分为 PERC 百分比费用模式 和 FIXED 固定费用模式 ;
Stock-like 模式与 PERC 百分比费用模式对应,期货 Futures-like 与 FIXED 固定费用模式对应;
在设置交易费用时,最常涉及如下 3 个参数:
commission:手续费 / 佣金;
mult:乘数;
margin:保证金 / 保证金比率 。
双边征收:买入和卖出操作都要收取相同的交易费用 。
cerebro.broker.setcommission(
# 交易手续费,根据margin取值情况区分是百分比手续费还是固定手续费
commission=0.0,
# 期货保证金,决定着交易费用的类型,只有在stocklike=False时起作用
margin=None,
# 乘数,盈亏会按该乘数进行放大
mult=1.0,
# 交易费用计算方式,取值有:
# 1.CommInfoBase.COMM_PERC 百分比费用
# 2.CommInfoBase.COMM_FIXED 固定费用
# 3.None 根据 margin 取值来确定类型
commtype=None,
# 当交易费用处于百分比模式下时,commission 是否为 % 形式
# True,表示不以 % 为单位,0.XX 形式;False,表示以 % 为单位,XX% 形式
percabs=True,
# 是否为股票模式,该模式通常由margin和commtype参数决定
# margin=None或COMM_PERC模式时,就会stocklike=True,对应股票手续费;
# margin设置了取值或COMM_FIXED模式时,就会stocklike=False,对应期货手续费
stocklike=False,
# 计算持有的空头头寸的年化利息
# days * price * abs(size) * (interest / 365)
interest=0.0,
# 计算持有的多头头寸的年化利息
interest_long=False,
# 杠杆比率,交易时按该杠杆调整所需现金
leverage=1.0,
# 自动计算保证金
# 如果False,则通过margin参数确定保证金
# 如果automargin<0,通过mult*price确定保证金
# 如果automargin>0,如果automargin*price确定保证金
automargin=False,
# 交易费用设置作用的数据集(也就是作用的标的)
# 如果取值为None,则默认作用于所有数据集(也就是作用于所有assets)
name=None)
从上述各参数的含义和作用可知,margin 、commtype、stocklike 存在 2 种默认的配置规则:股票百分比费用、期货固定费用,具体如下:
第 1 条规则:未设置 margin(即 margin 为 0 / None / False)→ commtype 会指向 COMM_PERC 百分比费用 → 底层的 _stocklike 属性会设置为 True → 对应的是“股票百分比费用”。
所以如果想为股票设置交易费用,就令 margin = 0 / None / False,或者令 stocklike=True;
第 2 条规则:为 margin 设置了取值 → commtype 会指向 COMM_FIXED 固定费用 → 底层的 _stocklike 属性会设置为 False → 对应的是“期货固定费用”,因为只有期货才会涉及保证金。
所以如果想为期货设置交易费用,就需要设置 margin,此外还需令 stocklike=True,margin 参数才会起作用 。
自定义交易费用的例子
# 自定义期货百分比费用
class CommInfo_Fut_Perc_Mult(bt.CommInfoBase):
params = (
('stocklike', False), # 指定为期货模式
('commtype', bt.CommInfoBase.COMM_PERC), # 使用百分比费用
('percabs', False), # commission 以 % 为单位
)
def _getcommission(self, size, price, pseudoexec):
# 计算交易费用
return (abs(size) * price) * (self.p.commission/100) * self.p.mult
# pseudoexec 用于提示当前是否在真实统计交易费用
# 如果只是试算费用,pseudoexec=False
# 如果是真实的统计费用,pseudoexec=True
comminfo = CommInfo_Fut_Perc_Mult(
commission=0.1, # 0.1%
mult=10,
margin=2000) # 实例化
cerebro.broker.addcommissioninfo(comminfo)
# 上述自定义函数,也可以通过 setcommission 来实现
cerebro.broker.setcommission(commission=0.1, #0.1%
mult=10,
margin=2000,
percabs=False,
commtype=bt.CommInfoBase.COMM_PERC,
stocklike=False)
下面是考虑佣金和印花税的股票百分比费用:
class StockCommission(bt.CommInfoBase):
params = (
('stocklike', True), # 指定为期货模式
('commtype', bt.CommInfoBase.COMM_PERC), # 使用百分比费用模式
('percabs', True), # commission 不以 % 为单位
('stamp_duty', 0.001),) # 印花税默认为 0.1%
# 自定义费用计算公式
def _getcommission(self, size, price, pseudoexec):
if size > 0: # 买入时,只考虑佣金
return abs(size) * price * self.p.commission
elif size < 0: # 卖出时,同时考虑佣金和印花税
return abs(size) * price * (self.p.commission + self.p.stamp_duty)
else:
return 0
成交量限制管理
形式1:bt.broker.fillers.FixedSize(size)
通过 FixedSize() 方法设置最大的固定成交量:size,该种模式下的成交量限制规则如下:
订单实际成交量的确定规则:取(size、订单执行那天的 volume 、订单中要求的成交数量)中的最小者;
订单执行那天,如果订单中要求的成交数量无法全部满足,则只成交部分数量。第二天不会补单。
# 通过 BackBroker() 类直接设置
cerebro = Cerebro()
filler = bt.broker.fillers.FixedSize(size=xxx)
newbroker = bt.broker.BrokerBack(filler=filler)
cerebro.broker = newbroker
# 通过 set_filler 方法设置
cerebro = Cerebro()
cerebro.broker.set_filler(bt.broker.fillers.FixedSize(size=xxx))
# self.order = self.buy(size=2000) # 每次买入 2000 股
# cerebro.broker.set_filler(bt.broker.fillers.FixedSize(size=3000)) # 固定最大成交量
形式2:bt.broker.fillers.FixedBarPerc(perc)
通过 FixedBarPerc(perc) 将 订单执行当天 bar 的总成交量 volume 的 perc % 设置为最大的固定成交量,该模式的成交量限制规则如下:
订单实际成交量的确定规则:取 (volume * perc /100、订单中要求的成交数量) 的 最小者;
订单执行那天,如果订单中要求的成交数量无法全部满足,则只成交部分数量。
# 通过 BackBroker() 类直接设置
cerebro = Cerebro()
filler = bt.broker.fillers.FixedBarPerc(perc=xxx)
newbroker = bt.broker.BrokerBack(filler=filler)
cerebro.broker = newbroker
# 通过 set_filler 方法设置
cerebro = Cerebro()
cerebro.broker.set_filler(bt.broker.fillers.FixedBarPerc(perc=xxx))
# perc 以 % 为单位,取值范围为[0.0,100.0]
# self.order = self.buy(size=2000) # 以下一日开盘价买入2000股
# cerebro.broker.set_filler(bt.broker.fillers.FixedBarPerc(perc=50))
形式3:bt.broker.fillers.BarPointPerc(minmov=0.01,perc=100.0)
BarPointPerc() 在考虑了价格区间的基础上确定成交量,在订单执行当天,成交量确定规则为:
通过 minmov 将 当天 bar 的价格区间 low ~ high 进行均匀划分,得到划分的份数:
part = (high -low +minmov) // minmov (向下取整)
再对当天 bar 的总成交量 volume 也划分成相同的份数 part ,这样就能得到每份的平均成交量:
volume_per = volume // part
最终,volume_per * (perc / 100)就是允许的最大成交量,实际成交时,对比订单中要求的成交量,就可以得到最终实际成交量
实际成交量 = min ( volume_per * (perc / 100), 订单中要求的成交数量 )
# 通过 BackBroker() 类直接设置
cerebro = Cerebro()
filler = bt.broker.fillers.BarPointPerc(minmov=0.01,perc=100.0)
newbroker = bt.broker.BrokerBack(filler=filler)
cerebro.broker = newbroker
# 通过 set_filler 方法设置
cerebro = Cerebro()
cerebro.broker.set_filler(bt.broker.fillers.BarPointPerc(minmov=0.01,perc=100.0))
# perc 以 % 为单位,取值范围为[0.0,100.0]
# self.order = self.buy(size=2000) # 以下一日开盘价买入2000股
# cerebro.broker.set_filler(bt.broker.fillers.BarPointPerc(minmov=0.1, perc=50)) # 表示 50%
交易时机管理
对于交易订单生成和执行时间,Backtrader 默认是 “当日收盘后下单,次日以开盘价成交”,这种模式在回测过程中能有效避免使用未来数据。
但对于一些特殊的交易场景,比如“all_in”情况下,当日所下订单中的数量是用当日收盘价计算的(总资金 / 当日收盘价),次日以开盘价执行订单时,
如果开盘价比昨天的收盘价提高了,就会出现可用资金不足的情况。
为了应对一些特殊交易场景,Backtrader 还提供了一些 cheating 式的交易时机模式:Cheat-On-Open 和 Cheat-On-Close。
Cheat-On-Open
Cheat-On-Open 是“当日下单,当日以开盘价成交”模式,在该模式下,Strategy 中的交易逻辑不再写在 next() 方法里,而是写在特定的 next_open()、nextstart_open() 、prenext_open() 函数中,具体设置可参考如下案例:
方式1:bt.Cerebro(cheat_on_open=True);
方式2:cerebro.broker.set_coo(True);
方式3:BackBroker(coo=True)。
Cheat-On-Close
Cheat-On-Close 是“当日下单,当日以收盘价成交”模式,在该模式下,Strategy 中的交易逻辑仍写在 next() 中,具体设置如下:
方式1:cerebro.broker.set_coc(True);
方式2:BackBroker(coc=True)
class TestStrategy(bt.Strategy):
......
def next(self):
# 取消之前未执行的订单
if self.order:
self.cancel(self.order)
# 检查是否有持仓
if not self.position:
# 10日均线上穿5日均线,买入
if self.crossover > 0:
print('{} Send Buy, open {}'.format(self.data.datetime.date(),self.data.open[0]))
self.order = self.buy(size=100) # 以下一日开盘价买入100股
# # 10日均线下穿5日均线,卖出
elif self.crossover < 0:
self.order = self.close() # 平仓,以下一日开盘价卖出
......
# 实例化大脑
cerebro= bt.Cerebro()
.......
# 当日下单,当日收盘价成交
cerebro.broker.set_coc(True)