量化交易学习(六十一)backtrader网格交易策略

震荡行情很适合用网格交易策略,今天这篇文章介绍怎么在backtrader中实现网格交易策略。

网格交易基本思想:

如果判断当前股票价格处于震荡阶段,则可根据历史行情找到震荡区间,然后按把震荡区间若干等分。

9fVRtVbNnYaFZ9CxWUguf.png

每当价格下跌一个档位则买入一份股票,每当价格上涨一个档位则卖出一份股票,如果价格下跌或上涨n个档位则买入或卖出n份股票。

Oiw9RDL37o6FpOmiyt3ya.png

用backtrader实现网格交易策略的回测

为了让我们的网格策略更加通用,可以用params定义参数:

1
2
3
4
5
6
7
8
9
10
11
class GridStrategy(bt.Strategy):
params=(
# 设置档位总数
('number', 10),
# 设置初始仓位
('open_percent', 0.5),
# 设置挡位间距
('distance', 0),
# 设置基准价格
('base_price',0)
)

在初始化方法中定义要用的变量:

1
2
3
4
5
6
7
8
def __init__(self):
# 设置初始订单状态
self.open_flg=False
self.last_index = 0
self.per_size=0
self.max_index = 0
self.min_index = 0
self.order=None

在next方法分为两个部分:建仓,和调仓。一般在基准价格附近建一半仓,然后随着价格的变动调仓。

首先进行调仓的判断,这样可以避免建仓后马上调仓:

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
def next(self):
# 判断是否已买入初始订单
if self.open_flg:
# 计算今日挡位
index = (self.data.close[0] - self.p.base_price) // self.p.distance

# 如果今日挡位低于下边界
if index < self.min_index:
# 用下边界替代今日挡位
index = self.min_index
# 如果当前挡位高于上边界
elif index > self.max_index:
# 用上边界替代今日挡位
index = self.max_index

self.log("上一交易日挡位:{}".format(self.last_index))
self.log("当前交易日挡位:{}".format(index))

# 计算挡位变化数
change_index = index - self.last_index
# 如果挡位变化数大于0
if change_index > 0:
# 执行卖出
self.sell(data=self.data, size=change_index*self.per_size)
# 如果挡位变化数小于0
elif change_index < 0:
# 执行买入
self.buy(data=self.data, size=change_index*self.per_size)

# 更新前一日挡位
self.last_index = index

调仓的判断完成后,再判断当前是否满足建仓的条件,满足则建仓:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 判断是否已买入初始订单
if not self.open_flg and math.fabs(self.data.close[0]-self.p.base_price)/self.p.base_price < 0.01:

# 计算所需买入的初始订单数量
buy_size = self.broker.getvalue() / self.data.close[0] * self.p.open_percent // 100 * 100
# 执行买入
self.buy(data=self.data, size=buy_size)

# 记录前一交易日的挡位,初始挡位是0
self.last_index = 0
# 计算每变化一挡对应的订单数量
self.per_size = self.broker.getvalue() / self.data.close[0] / self.p.number // 100 * 100
# 计算档位的上边界
self.max_index = round(self.p.number * self.p.open_percent)
# 计算档位的下边界,由于在初始挡位的下方,所以结果是负数
self.min_index = self.max_index - self.p.number

# 更新初始订单状态
self.open_flg = True
self.log('已买入初始订单')

完整的策略代码如下:

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import backtrader as bt
import math

class GridStrategy(bt.Strategy):
params=(
# 设置档位总数
('number', 10),
# 设置初始仓位
('open_percent', 0.5),
# 设置挡位间距
('distance', 0),
# 设置基准价格
('base_price',0)
)

def __init__(self):
# 设置初始订单状态
self.open_flg=False
self.last_index = 0
self.per_size=0
self.max_index = 0
self.min_index = 0
self.order=None


def log(self, txt, dt=None):
dt = dt or self.data.datetime[0]
dt = bt.num2date(dt).date()
print(f'{dt}: {txt}')

def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# 订单状态 submitted/accepted,无动作
return

# 订单完成
if order.status in [order.Completed]:
if order.isbuy():
self.log('买单执行,%s, %.3f, %i' % (order.data._name,
order.executed.price, order.executed.size))

elif order.issell():
self.log('卖单执行, %s, %.3f, %i' % (order.data._name,
order.executed.price, order.executed.size))
print('佣金 %.2f, 市值 %.2f, 现金 %.2f' %
( order.executed.comm, self.broker.getvalue(), self.broker.getcash()))

else:
self.log('订单作废 %s, %s, isbuy=%i, size %i, open price %.2f' %
(order.data._name, order.getstatusname(), order.isbuy(), order.created.size, order.data.open[0]))
self.order=None # 重置订单状态

def next(self):
# 判断是否已买入初始订单
if self.open_flg:
# 计算今日挡位
index = (self.data.close[0] - self.p.base_price) // self.p.distance

# 如果今日挡位低于下边界
if index < self.min_index:
# 用下边界替代今日挡位
index = self.min_index
# 如果当前挡位高于上边界
elif index > self.max_index:
# 用上边界替代今日挡位
index = self.max_index

self.log("上一交易日挡位:{}".format(self.last_index))
self.log("当前交易日挡位:{}".format(index))

# 计算挡位变化数
change_index = index - self.last_index
# 如果挡位变化数大于0
if change_index > 0:
# 执行卖出
self.sell(data=self.data, size=change_index*self.per_size)
# 如果挡位变化数小于0
elif change_index < 0:
# 执行买入
self.buy(data=self.data, size=change_index*self.per_size)

# 更新前一日挡位
self.last_index = index
# 判断是否已买入初始订单
if not self.open_flg and math.fabs(self.data.close[0]-self.p.base_price)/self.p.base_price < 0.01:

# 计算所需买入的初始订单数量
buy_size = self.broker.getvalue() / self.data.close[0] * self.p.open_percent // 100 * 100
# 执行买入
self.buy(data=self.data, size=buy_size)

# 记录前一交易日的挡位,初始挡位是0
self.last_index = 0
# 计算每变化一挡对应的订单数量
self.per_size = self.broker.getvalue() / self.data.close[0] / self.p.number // 100 * 100
# 计算档位的上边界
self.max_index = round(self.p.number * self.p.open_percent)
# 计算档位的下边界,由于在初始挡位的下方,所以结果是负数
self.min_index = self.max_index - self.p.number

# 更新初始订单状态
self.open_flg = True
self.log('已买入初始订单')


self.log("当前持仓规模:{},市值:{},现金:{}".format(self.getposition(self.data).size,self.broker.getvalue(), self.broker.getcash()))

main.py代码如下:

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
from backtrader import bt
import datetime

import mysqlDataFeed
import strategy


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

def main(symbol):
cerebro = bt.Cerebro()
cerebro.addstrategy(strategy.GridStrategy,base_price=0.85,distance=0.01)
today=datetime.datetime.today()
data=mysqlDataFeed.MySQLData(
dataname=symbol,
timeframe=bt.TimeFrame.Days,
fromdate=today-relativedelta(months=6),
todate=today,
adj_base_date=today
)
cerebro.adddata(data)

comminfo = EastMoneyCommission(
dismiss5=True,
commission=0.00015,
stamp_duty=0,
transfer_fee=0,
transaction_fees=0
)

cerebro.broker.addcommissioninfo(comminfo)
cerebro.broker.setcash(100000)
cerebro.run()
cerebro.plot()


if __name__=='__main__':
print(datetime.datetime.now())
main('SHSE.512720')
print(datetime.datetime.now())

数据源的代码就不再贴了,大家可以看 量化交易学习(二十四)下载股票数据保存到数据库、量化交易学习(二十五)backtrader使用mysql数据源 这两篇文章,

我用最近6个月的计算机ETF的数据进行了回测,基准价设为0.85,价格档位设为0.01,回测结果如下:

Figure_0.png

Snipaste_2024-05-30_16-44-32.png

Snipaste_2024-05-30_16-43-46.png


希望这篇文章能帮助到大家。如果你有任何问题或建议,欢迎留言讨论,私信。感谢你的阅读,觉得不错,点个赞哦!还没有关注我的朋友可以关注 江达小记

江达小记