量化交易学习(十三)backtrader佣金设置

回测时,佣金的设置有时会被忽略,然而在频繁交易时过高的佣金会极大地影响收益,只有正确设置佣金才能让我们的回测更贴合实际。

交易佣金的收取规则

首先来看一下交易佣金的收取规则(参考东方财富的帮助文档):

税费合计为个股交易费用和股息红利税费之和,其中不同证券品种的交易费用构成如下:

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),# 佣金少于5块时免不免5
('commission',0.00015), # 佣金费率万分之1.5
('stamp_duty',0.0005), # 印花税 万分之5
('transfer_fee',0.00001), # 过户费 十万分之1
('transaction_fees',0.0000541), # 交易规费
('percabs',True) # 为True则使用小数,为False则使用百分数
)

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
# 是否免5
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))

# 每条k线都会执行这个函数
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元收取的。我们自定义的佣金类可以按预期执行。

江达小记