回测时,佣金的设置有时会被忽略,然而在频繁交易时过高的佣金会极大地影响收益,只有正确设置佣金才能让我们的回测更贴合实际。
交易佣金的收取规则
首先来看一下交易佣金的收取规则(参考东方财富的帮助文档):
税费合计为个股交易费用和股息红利税费之和,其中不同证券品种的交易费用构成如下:
1、沪深A股
交易费用=佣金+印花税+过户费,收费标准如下:
(1)佣金=成交金额x交易佣金率=净佣金+交易规费,股票佣金不足5元按5元收取。
(2)印花税=成交金额x0.05%,仅卖出时收取
(3)过户费=成交金额×0.001%,沪市单独收取,深市包含在交易规费中
(4)其他费用,一般是指交易规费或场内基金申购(赎回)费,交易规费包含在佣金中不再单独收取。
沪市交易规费=经手费+证管费=成交金额×(0.00341%+0.002%)=成交金额×0.00541%
深市交易规费=经手费+证管费+过户费=成交金额×0.00641%
2、北交所、新三板、两网及退市股份
交易费用=佣金+印花税,收费标准如下:
(1)佣金=成交金额x交易佣金率,股票佣金不足5元按5元收取。
(2)印花税=成交金额x0.05%(仅卖出时收取)
3、港股通
交易费用=佣金+印花税+交易征费+交易费+会财局交易征费+股份交收费,各收费项目均按照港币计算,实际收取时根据结算汇率转换成人民币。
收费标准如下:
(1)佣金=成交金额x交易佣金率,佣金不足5港元按5港元收取
(2)印花税=成交金额x0.1%,双边收取,取整到元,不足1港元按1港元收取
(3)交易征费=成交金额×0.0027%,双边收取
(4)交易费=成交金额x0.00565%,双边收取
(5) 会财局交易征费=成交金额x0.00015%,双边收取
(6)股份交收费=成交金额x0.002%,双边收取,每边最低收取2港元,最高收取100港元
(7)证券组合费,根据投资者证券账户持有市值以一定比例按自然日计算收取,不计入单只持仓个股的交易费用中
4、债券及国债逆回购
交易费用仅收取佣金
5、ETF、LOF、REITs等场内基金
交易费用仅收取佣金
自定义佣金类
可以看到,对于不同市场,不同的证券类别,它们的佣金是不同的。计算方法也是不同的,backtrader默认的佣金类只考虑了最简单的情况,我们要根据自己的券商定义自己的佣金类。这里我以沪市A股为例,券商佣金费率设为万1.5:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class EastMoneyCommission(bt.CommInfoBase): params = ( ('dismiss5',False), ('commission',0.00015), ('stamp_duty',0.0005), ('transfer_fee',0.00001), ('transaction_fees',0.0000541), ('percabs',True) )
def _getcommission(self, size, price, pseudoexec): if size>0: fee= size*price * (self.p.commission+self.p.transaction_fees+self.p.transfer_fee) elif size<0: fee = -size * price * (self.p.commission + self.p.transaction_fees + self.p.stamp_duty + self.p.transfer_fee) else: return 0 if self.params.dismiss5: return fee else: if fee < 5: return 5 return fee
|
跑一个双均线策略来检验下我们的佣金设置对不对:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| class DoubleMAStrategy(bt.Strategy): params = ( ('short_period', 20), ('long_period', 60), ('start_date', None), ('stock_percent',0.9), )
def log(self, txt, dt=None): dt = dt or self.datas[0].datetime.date(0) print('%s, %s' % (dt.isoformat(), txt))
def __init__(self): self.dataclose = self.data.close self.order = None self.buyprice = None self.buycomm = None self.smashort = bt.indicators.SimpleMovingAverage( self.data, period=self.params.short_period ) self.smalong = bt.indicators.SimpleMovingAverage( self.data, period=self.params.long_period )
self.cross = bt.indicators.CrossOver(self.smashort, self.smalong) self.start_date = datetime.strptime(self.p.start_date, '%Y-%m-%d %H:%M:%S') if self.p.start_date is not None else None
def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: return
if order.status in [order.Completed]: if order.isbuy(): self.log( '买入已执行,价格为:%.2f,花费:%.2f,佣金:%.2f' % (order.executed.price, order.executed.value, order.executed.comm)) self.buyprice = order.executed.price self.buycomm = order.executed.comm else: self.log( '卖出已执行,价格为:%.2f,花费:%.2f,佣金:%.2f' % (order.executed.price, order.executed.value, order.executed.comm))
self.bar_executed = len(self) elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log('Order Canceled/Margin/Rejected')
self.order = None
def notify_trade(self, trade): if not trade.isclosed: return
self.log('当前盈亏 %.2f ,去除佣金后的盈亏 %.2f' % (trade.pnl, trade.pnlcomm))
def next(self): if self.start_date is not None and self.start_date > bt.utils.num2date(self.data.datetime[0]): return if self.order: return if not self.position: if self.cross == 1: vol = self.broker.getvalue()*self.p.stock_percent / self.dataclose[0] // 100 * 100 self.log('创建买单,%.2f' % self.dataclose[0]) self.order = self.buy(size=vol) else: if self.cross == -1: self.log('创建卖单,%.2f' % self.dataclose[0]) self.order = self.sell(size=self.position.size)
|
backtrader主函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| def backtrader(conn): cerebro = bt.Cerebro()
cerebro.addstrategy(DoubleMAStrategy, short_period=20, long_period=60, start_date='2020-01-01 16:00:00')
cerebro.adddata(MyQuantData(conn=conn, name='SHSE.601728'))
cerebro.broker.setcash(100000)
comminfo = EastMoneyCommission( dismiss5=False, commission=0.00015, stamp_duty=0.0005, transfer_fee=0.00001, transaction_fees=0.0000541 )
cerebro.broker.addcommissioninfo(comminfo)
print('初始时资金持仓:%.2f' % cerebro.broker.getvalue()) cerebro.run() print('结束时资金持仓:%.2f' % cerebro.broker.getvalue()) cerebro.plot()
|
当初始资金为10万时,回测结果如下:

以最初的两次交易为例,我们来手动计算下交易费用是否算对了:

2021-12-14日触发买单,以2021-12-15日的开盘价3.87买入23200股:
佣金计算如下:3.8723200(券商佣金费率0.00015+沪市交易规费0.0000541+过户费0.00001)=19.2227
2022-01-25日触发卖单,以2022-01-26日的开盘价3.75卖出23200股:
佣金计算如下:3.7523200(券商佣金费率0.00015+沪市交易规费0.0000541+过户费0.00001+印花税0.0005)=62.1267
买入的交易费用与手动计算有2分钱的误差,现在我还没找到原因,卖出的交易费用在四舍五入之后结果是一样的。
另外在程序执行时获得的卖出的花费是错的,也还没找到原因,不过不影响回测。
接下来再看看交易费用不足5块时,能不能得到正确的佣金数。
把回测开始时的现金数改成1万元:

2021-12-14日触发买单,以2021-12-15日的开盘价3.87买入2300股:
佣金计算如下:3.872300(券商佣金费率0.00015+沪市交易规费0.0000541+过户费0.00001)=1.9057
2022-01-25日触发卖单,以2022-01-26日的开盘价3.75卖出2300股:
佣金计算如下:3.752300(券商佣金费率0.00015+沪市交易规费0.0000541+过户费0.00001+印花税0.0005)=6.1591
可以看到当佣金小于5元时,还是按5元收取的。我们自定义的佣金类可以按预期执行。
