订单管理和执行
原文:
www.backtrader.com/blog/posts/2015-08-08-order-creation-execution/order-creation-execution/
回测,因此 backtrader,如果不能模拟订单,将不完整。为此,平台提供了以下功能。
对于订单管理有 3 个原语:
-
购买
-
卖出
-
取消
注意
update
原语显然是一种逻辑,但常识告诉我们,这种方法主要由使用判断性交易方法的手动操作员使用。
对于订单执行逻辑,以下执行类型:
-
市价
-
关闭
-
限价
-
停止
-
StopLimit
订单管理
主要目标是易于使用,因此进行订单管理的最直接(和简单)的方法是从策略本身开始。
buy
和self
原语具有以下签名作为Strategy
方法:
-
def buy(self, data=None, size=None, price=None, plimit=None, exectype=None, valid=None):
-
def buy(self, data=None, size=None, price=None, exectype=None, valid=None)
data
-> 数据源引用,用于购买的资产
如果传递
None
,则策略的主要数据将用作目标size
-> 确定要应用的股份的 int/long
如果传递
None
,则策略中可用的Sizer
将被用于自动确定股份。默认的Sizer
使用固定状态1-
price
-> 对于Market
将被忽略并且可以留空,但对于其他订单类型必须是浮点数。如果留空,则将使用当前收盘价 -
plimit
-> 在StopLimit
订单中的限价,其中price
将被用作触发价格
如果留空,则
price
将被用作限价(触发和限价相同)exectype
-> 订单执行类型之一。如果传递None
,则将假定为Market
执行类型在
Order
中被枚举。例如:Order.Limit
valid
-> 从 date2num(或数据源)获取的浮点值或 datetime.datetime Python 对象
注意:
Market
订单将在不考虑valid
参数的情况下执行返回值:一个
Order
实例 -
def sell(self, data=None, size=None, price=None, exectype=None, valid=None)
因为取消订单只需要buy
或self
返回的order
引用,所以经纪人的原语可以使用(见下文)
一些例子:
# buy the main date, with sizer default stake, Market order
order = self.buy()
# Market order - valid will be "IGNORED"
order = self.buy(valid=datetime.datetime.now() + datetime.timedelta(days=3))
# Market order - price will be IGNORED
order = self.buy(price=self.data.close[0] * 1.02)
# Market order - manual stake
order = self.buy(size=25)
# Limit order - want to set the price and can set a validity
order = self.buy(exectype=Order.Limit,
price=self.data.close[0] * 1.02,
valid=datetime.datetime.now() + datetime.timedelta(days=3)))
# StopLimit order - want to set the price, price limit
order = self.buy(exectype=Order.StopLimit,
price=self.data.close[0] * 1.02,
plimit=self.data.close[0] * 1.07)
# Canceling an existing order
self.broker.cancel(order)
注意
所有订单类型都可以通过创建Order
实例(或其子类之一),然后通过以下方式传递给经纪人:
order = self.broker.submit(order)
注意
broker
本身有buy
和sell
原语,但对于默认参数要求更严格。
订单执行逻辑
broker
使用 2 个主要准则(假设?)进行订单执行。
-
当前数据已经发生,不能用于执行订单。
如果策略中的逻辑如下:
if self.data.close > self.sma: # where sma is a Simple Moving Average self.buy() The expectation CANNOT be that the order will be executed with the ``close`` price which is being examined in the logic BECAUSE it has already happened. The order CAN BE 1st EXECUTED withing the bounds of the next set of Open/High/Low/Close price points (and the conditions set forth herein by the order)`
-
交易量不起作用
实际上,在真实交易中,如果交易员选择非流动性资产,或者精确地击中了价格条的极值(高/低),订单将被取消。
但是击中高/低点是个罕见的事件(如果你这么做...你就不需要
backtrader
了),并且所选择的资产将有足够的流动性来吸收任何常规交易的订单
市场
执行:
下一个一组 Open/High/Low/Close 价格(通常称为bar)的开盘价
理由:
如果逻辑在时间点 X 执行并发出了Market
订单,则接下来会发生的价格点是即将到来的open
价格
注意
这个订单总是执行,忽略任何用于创建它的price
和valid
参数
关闭
执行:
当下一个价格条实际上关闭时,使用下一个价格条的close
价格
理由:
大多数回测数据源已经包含了已关闭的价格条,订单将立即执行,使用下一个价格条的close
价格。每日数据源是最常见的例子。
但是系统可以被喂入“tick”价格,实际的条(按时间/日期)正在不断地随着新的 ticks 更新,而实际上并没有移动到下一个条(因为时间和/或日期没有改变)
只有当时间或日期改变时,条才会实际上被关闭,订单才会执行
限价
执行:
订单创建时设置的price
,如果data
触及它,则从下一个价格条开始。
如果设置了valid
并且到达了时间点,订单将被取消
价格匹配:
backtrader
试图为Limit
订单提供最真实的执行价格。
使用 4 个价格点(Open/High/Low/Close),可以部分推断请求的price
是否可以改善。
对于Buy
订单
- Case 1:
If the `open` price of the bar is below the limit price the order
executes immediately with the `open` price. The order has been swept
during the opening phase of the session
- Case 2:
If the `open` price has not penetrated below the limit price but the
`low` price is below the limit price, then the limit price has been
seen during the session and the order can be executed
对于Sell
订单,逻辑显然是相反的。
止损
执行:
订单创建时设置的触发器price
,如果data
触及它,则从下一个价格条开始。
如果设置了valid
并且到达了时间点,订单将被取消
价格匹配:
backtrader
试图为Stop
订单提供最真实的触发价格。
使用 4 个价格点(Open/High/Low/Close),可以部分推断请求的price
是否可以改善。
对于\
Stoporders which
Buy`
- Case 1:
If the `open` price of the bar is above the stop price the order is
executed immediately with the `open` price.
Intended to stop a loss if the price is moving upwards against an
existing short position
- Case 2:
If the `open` price has not penetrated above the stop price but the
`high` price is above the stop price, then the stop price has been
seen during the session and the order can be executed
对于Sell
订单,逻辑显然是相反的。
止损限价
执行:
触发器price
设置了订单的启动,从下一个价格条开始。
价格匹配:
-
触发器:使用
Stop
匹配逻辑(但仅触发并将订单转换为Limit
订单) -
限制:使用
Limit
价格匹配逻辑
一些样本
像往常一样,图片(带代码)价值数百万长的解释。请注意,代码片段集中在订单创建部分。完整代码在底部。
一个价格高于/低于简单移动平均线策略将用于生成买入/卖出信号
信号在图表底部可见:使用交叉指标的CrossOver
。
仅保留对生成的“买入”订单的引用,以允许系统中最多只有一个同时订单。
执行类型:市价
请在图表中查看订单如何在生成信号后一根棒棒后以开盘价执行。
if self.p.exectype == 'Market':
self.buy(exectype=bt.Order.Market) # default if not given
self.log('BUY CREATE, exectype Market, price %.2f' %
self.data.close[0])
输出图表。
命令行和输出:
$ ./order-execution-samples.py --exectype Market
2006-01-26T23:59:59+00:00, BUY CREATE, exectype Market, price 3641.42
2006-01-26T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-01-27T23:59:59+00:00, BUY EXECUTED, Price: 3643.35, Cost: 3643.35, Comm 0.00
2006-03-02T23:59:59+00:00, SELL CREATE, 3763.73
2006-03-02T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-03T23:59:59+00:00, SELL EXECUTED, Price: 3763.95, Cost: 3763.95, Comm 0.00
...
...
2006-12-11T23:59:59+00:00, BUY CREATE, exectype Market, price 4052.89
2006-12-11T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-12-12T23:59:59+00:00, BUY EXECUTED, Price: 4052.55, Cost: 4052.55, Comm 0.00
执行类型:收盘
注意
在Issue#11之后,创建了一个开发分支,更新了图表和输出。错误的收盘价格被使用。
现在订单也是在信号后一根棒棒后执行,但是使用的是收盘价。
elif self.p.exectype == 'Close':
self.buy(exectype=bt.Order.Close)
self.log('BUY CREATE, exectype Close, price %.2f' %
self.data.close[0])
输出图表。
命令行和输出:
$ ./order-execution-samples.py --exectype Close
2006-01-26T23:59:59+00:00, BUY CREATE, exectype Close, price 3641.42
2006-01-26T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-01-27T23:59:59+00:00, BUY EXECUTED, Price: 3685.48, Cost: 3685.48, Comm 0.00
2006-03-02T23:59:59+00:00, SELL CREATE, 3763.73
2006-03-02T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-03T23:59:59+00:00, SELL EXECUTED, Price: 3763.95, Cost: 3763.95, Comm 0.00
...
...
2006-11-06T23:59:59+00:00, BUY CREATE, exectype Close, price 4045.22
2006-11-06T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-11-07T23:59:59+00:00, BUY EXECUTED, Price: 4072.86, Cost: 4072.86, Comm 0.00
2006-11-24T23:59:59+00:00, SELL CREATE, 4048.16
2006-11-24T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-11-27T23:59:59+00:00, SELL EXECUTED, Price: 4045.05, Cost: 4045.05, Comm 0.00
2006-12-11T23:59:59+00:00, BUY CREATE, exectype Close, price 4052.89
2006-12-11T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-12-12T23:59:59+00:00, BUY EXECUTED, Price: 4059.74, Cost: 4059.74, Comm 0.00
执行类型:限价
在几行之前计算有效性以防已作为参数传递。
if self.p.valid:
valid = self.data.datetime.date(0) + \
datetime.timedelta(days=self.p.valid)
else:
valid = None
设置了信号生成价格(信号条的收盘价)下跌 1%的限价。请注意,这阻止了上面许多订单的执行。
elif self.p.exectype == 'Limit':
price = self.data.close * (1.0 - self.p.perc1 / 100.0)
self.buy(exectype=bt.Order.Limit, price=price, valid=valid)
if self.p.valid:
txt = 'BUY CREATE, exectype Limit, price %.2f, valid: %s'
self.log(txt % (price, valid.strftime('%Y-%m-%d')))
else:
txt = 'BUY CREATE, exectype Limit, price %.2f'
self.log(txt % price)
输出图表。
仅发布了 4 个订单。尝试捕捉小幅度下跌的价格完全改变了输出。
命令行和输出:
$ ./order-execution-samples.py --exectype Limit --perc1 1
2006-01-26T23:59:59+00:00, BUY CREATE, exectype Limit, price 3605.01
2006-01-26T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-05-18T23:59:59+00:00, BUY EXECUTED, Price: 3605.01, Cost: 3605.01, Comm 0.00
2006-06-05T23:59:59+00:00, SELL CREATE, 3604.33
2006-06-05T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-06-06T23:59:59+00:00, SELL EXECUTED, Price: 3598.58, Cost: 3598.58, Comm 0.00
2006-06-21T23:59:59+00:00, BUY CREATE, exectype Limit, price 3491.57
2006-06-21T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-06-28T23:59:59+00:00, BUY EXECUTED, Price: 3491.57, Cost: 3491.57, Comm 0.00
2006-07-13T23:59:59+00:00, SELL CREATE, 3562.56
2006-07-13T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-07-14T23:59:59+00:00, SELL EXECUTED, Price: 3545.92, Cost: 3545.92, Comm 0.00
2006-07-24T23:59:59+00:00, BUY CREATE, exectype Limit, price 3596.60
2006-07-24T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
执行类型:限价与有效期
为了不会永远等待一个限价订单,该订单仅在 4(日历)天内有效,该订单可能只有在价格对“买入”订单不利时才执行。
输出图表。
更多订单已生成,但除了一个“买入”订单过期外,其他所有订单都过期了,进一步限制了操作数量。
命令行和输出:
$ ./order-execution-samples.py --exectype Limit --perc1 1 --valid 4
2006-01-26T23:59:59+00:00, BUY CREATE, exectype Limit, price 3605.01, valid: 2006-01-30
2006-01-26T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-01-30T23:59:59+00:00, BUY EXPIRED
2006-03-10T23:59:59+00:00, BUY CREATE, exectype Limit, price 3760.48, valid: 2006-03-14
2006-03-10T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-14T23:59:59+00:00, BUY EXPIRED
2006-03-30T23:59:59+00:00, BUY CREATE, exectype Limit, price 3835.86, valid: 2006-04-03
2006-03-30T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-04-03T23:59:59+00:00, BUY EXPIRED
2006-04-20T23:59:59+00:00, BUY CREATE, exectype Limit, price 3821.40, valid: 2006-04-24
2006-04-20T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-04-24T23:59:59+00:00, BUY EXPIRED
2006-05-04T23:59:59+00:00, BUY CREATE, exectype Limit, price 3804.65, valid: 2006-05-08
2006-05-04T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-05-08T23:59:59+00:00, BUY EXPIRED
2006-06-01T23:59:59+00:00, BUY CREATE, exectype Limit, price 3611.85, valid: 2006-06-05
2006-06-01T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-06-05T23:59:59+00:00, BUY EXPIRED
2006-06-21T23:59:59+00:00, BUY CREATE, exectype Limit, price 3491.57, valid: 2006-06-25
2006-06-21T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-06-26T23:59:59+00:00, BUY EXPIRED
2006-07-24T23:59:59+00:00, BUY CREATE, exectype Limit, price 3596.60, valid: 2006-07-28
2006-07-24T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-07-28T23:59:59+00:00, BUY EXPIRED
2006-09-12T23:59:59+00:00, BUY CREATE, exectype Limit, price 3751.07, valid: 2006-09-16
2006-09-12T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-09-18T23:59:59+00:00, BUY EXPIRED
2006-09-20T23:59:59+00:00, BUY CREATE, exectype Limit, price 3802.90, valid: 2006-09-24
2006-09-20T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-09-22T23:59:59+00:00, BUY EXECUTED, Price: 3802.90, Cost: 3802.90, Comm 0.00
2006-11-02T23:59:59+00:00, SELL CREATE, 3974.62
2006-11-02T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-11-03T23:59:59+00:00, SELL EXECUTED, Price: 3979.73, Cost: 3979.73, Comm 0.00
2006-11-06T23:59:59+00:00, BUY CREATE, exectype Limit, price 4004.77, valid: 2006-11-10
2006-11-06T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-11-10T23:59:59+00:00, BUY EXPIRED
2006-12-11T23:59:59+00:00, BUY CREATE, exectype Limit, price 4012.36, valid: 2006-12-15
2006-12-11T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-12-15T23:59:59+00:00, BUY EXPIRED
执行类型:止损
设置了信号价格上涨 1%的止损价。这意味着策略仅在生成信号并价格继续上涨时购买,这可以被解释为强势信号。
这完全改变了执行情况。
elif self.p.exectype == 'Stop':
price = self.data.close * (1.0 + self.p.perc1 / 100.0)
self.buy(exectype=bt.Order.Stop, price=price, valid=valid)
if self.p.valid:
txt = 'BUY CREATE, exectype Stop, price %.2f, valid: %s'
self.log(txt % (price, valid.strftime('%Y-%m-%d')))
else:
txt = 'BUY CREATE, exectype Stop, price %.2f'
self.log(txt % price)
输出图表。
命令行和输出:
$ ./order-execution-samples.py --exectype Stop --perc1 1
2006-01-26T23:59:59+00:00, BUY CREATE, exectype Stop, price 3677.83
2006-01-26T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-01-27T23:59:59+00:00, BUY EXECUTED, Price: 3677.83, Cost: 3677.83, Comm 0.00
2006-03-02T23:59:59+00:00, SELL CREATE, 3763.73
2006-03-02T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-03T23:59:59+00:00, SELL EXECUTED, Price: 3763.95, Cost: 3763.95, Comm 0.00
2006-03-10T23:59:59+00:00, BUY CREATE, exectype Stop, price 3836.44
2006-03-10T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-15T23:59:59+00:00, BUY EXECUTED, Price: 3836.44, Cost: 3836.44, Comm 0.00
2006-03-28T23:59:59+00:00, SELL CREATE, 3811.45
2006-03-28T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-29T23:59:59+00:00, SELL EXECUTED, Price: 3811.85, Cost: 3811.85, Comm 0.00
2006-03-30T23:59:59+00:00, BUY CREATE, exectype Stop, price 3913.36
2006-03-30T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-09-29T23:59:59+00:00, BUY EXECUTED, Price: 3913.36, Cost: 3913.36, Comm 0.00
2006-11-02T23:59:59+00:00, SELL CREATE, 3974.62
2006-11-02T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-11-03T23:59:59+00:00, SELL EXECUTED, Price: 3979.73, Cost: 3979.73, Comm 0.00
2006-11-06T23:59:59+00:00, BUY CREATE, exectype Stop, price 4085.67
2006-11-06T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-11-13T23:59:59+00:00, BUY EXECUTED, Price: 4085.67, Cost: 4085.67, Comm 0.00
2006-11-24T23:59:59+00:00, SELL CREATE, 4048.16
2006-11-24T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-11-27T23:59:59+00:00, SELL EXECUTED, Price: 4045.05, Cost: 4045.05, Comm 0.00
2006-12-11T23:59:59+00:00, BUY CREATE, exectype Stop, price 4093.42
2006-12-11T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-12-13T23:59:59+00:00, BUY EXECUTED, Price: 4093.42, Cost: 4093.42, Comm 0.00
执行类型:止损限价
设置了信号价格上涨 1%的止损价。但限价设置在信号(收盘)价格上涨 0.5%,这可以解释为:等待力量展现,但不要买入高峰。等待下跌。
有效期限制为 20(日历)天
elif self.p.exectype == 'StopLimit':
price = self.data.close * (1.0 + self.p.perc1 / 100.0)
plimit = self.data.close * (1.0 + self.p.perc2 / 100.0)
self.buy(exectype=bt.Order.StopLimit, price=price, valid=valid,
plimit=plimit)
if self.p.valid:
txt = ('BUY CREATE, exectype StopLimit, price %.2f,'
' valid: %s, pricelimit: %.2f')
self.log(txt % (price, valid.strftime('%Y-%m-%d'), plimit))
else:
txt = ('BUY CREATE, exectype StopLimit, price %.2f,'
' pricelimit: %.2f')
self.log(txt % (price, plimit))
输出图表。
命令行和输出:
$ ./order-execution-samples.py --exectype StopLimit --perc1 1 --perc2 0.5 --valid 20
2006-01-26T23:59:59+00:00, BUY CREATE, exectype StopLimit, price 3677.83, valid: 2006-02-15, pricelimit: 3659.63
2006-01-26T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-02-03T23:59:59+00:00, BUY EXECUTED, Price: 3659.63, Cost: 3659.63, Comm 0.00
2006-03-02T23:59:59+00:00, SELL CREATE, 3763.73
2006-03-02T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-03T23:59:59+00:00, SELL EXECUTED, Price: 3763.95, Cost: 3763.95, Comm 0.00
2006-03-10T23:59:59+00:00, BUY CREATE, exectype StopLimit, price 3836.44, valid: 2006-03-30, pricelimit: 3817.45
2006-03-10T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-21T23:59:59+00:00, BUY EXECUTED, Price: 3817.45, Cost: 3817.45, Comm 0.00
2006-03-28T23:59:59+00:00, SELL CREATE, 3811.45
2006-03-28T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-29T23:59:59+00:00, SELL EXECUTED, Price: 3811.85, Cost: 3811.85, Comm 0.00
2006-03-30T23:59:59+00:00, BUY CREATE, exectype StopLimit, price 3913.36, valid: 2006-04-19, pricelimit: 3893.98
2006-03-30T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-04-19T23:59:59+00:00, BUY EXPIRED
...
...
2006-12-11T23:59:59+00:00, BUY CREATE, exectype StopLimit, price 4093.42, valid: 2006-12-31, pricelimit: 4073.15
2006-12-11T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-12-22T23:59:59+00:00, BUY EXECUTED, Price: 4073.15, Cost: 4073.15, Comm 0.00
测试脚本执行
在命令行的help
中详细说明:
$ ./order-execution-samples.py --help
usage: order-execution-samples.py [-h] [--infile INFILE]
[--csvformat {bt,visualchart,sierrachart,yahoo,yahoo_unreversed}]
[--fromdate FROMDATE] [--todate TODATE]
[--plot] [--plotstyle {bar,line,candle}]
[--numfigs NUMFIGS] [--smaperiod SMAPERIOD]
[--exectype EXECTYPE] [--valid VALID]
[--perc1 PERC1] [--perc2 PERC2]
Showcase for Order Execution Types
optional arguments:
-h, --help show this help message and exit
--infile INFILE, -i INFILE
File to be read in
--csvformat {bt,visualchart,sierrachart,yahoo,yahoo_unreversed},
-c {bt,visualchart,sierrachart,yahoo,yahoo_unreversed}
CSV Format
--fromdate FROMDATE, -f FROMDATE
Starting date in YYYY-MM-DD format
--todate TODATE, -t TODATE
Ending date in YYYY-MM-DD format
--plot, -p Plot the read data
--plotstyle {bar,line,candle}, -ps {bar,line,candle}
Plot the read data
--numfigs NUMFIGS, -n NUMFIGS
Plot using n figures
--smaperiod SMAPERIOD, -s SMAPERIOD
Simple Moving Average Period
--exectype EXECTYPE, -e EXECTYPE
Execution Type: Market (default), Close, Limit,
Stop, StopLimit
--valid VALID, -v VALID
Validity for Limit sample: default 0 days
--perc1 PERC1, -p1 PERC1
% distance from close price at order creation time for
the limit/trigger price in Limit/Stop orders
--perc2 PERC2, -p2 PERC2
% distance from close price at order creation time for
the limit price in StopLimit orders
完整代码
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import argparse
import datetime
import os.path
import time
import sys
import backtrader as bt
import backtrader.feeds as btfeeds
import backtrader.indicators as btind
class OrderExecutionStrategy(bt.Strategy):
params = (
('smaperiod', 15),
('exectype', 'Market'),
('perc1', 3),
('perc2', 1),
('valid', 4),
)
def log(self, txt, dt=None):
''' Logging function fot this strategy'''
dt = dt or self.data.datetime[0]
if isinstance(dt, float):
dt = bt.num2date(dt)
print('%s, %s' % (dt.isoformat(), txt))
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
self.log('ORDER ACCEPTED/SUBMITTED', dt=order.created.dt)
self.order = order
return
if order.status in [order.Expired]:
self.log('BUY EXPIRED')
elif order.status in [order.Completed]:
if order.isbuy():
self.log(
'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
else: # Sell
self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
# Sentinel to None: new orders allowed
self.order = None
def __init__(self):
# SimpleMovingAverage on main data
# Equivalent to -> sma = btind.SMA(self.data, period=self.p.smaperiod)
sma = btind.SMA(period=self.p.smaperiod)
# CrossOver (1: up, -1: down) close / sma
self.buysell = btind.CrossOver(self.data.close, sma, plot=True)
# Sentinel to None: new ordersa allowed
self.order = None
def next(self):
if self.order:
# An order is pending ... nothing can be done
return
# Check if we are in the market
if self.position:
# In the maerket - check if it's the time to sell
if self.buysell < 0:
self.log('SELL CREATE, %.2f' % self.data.close[0])
self.sell()
elif self.buysell > 0:
if self.p.valid:
valid = self.data.datetime.date(0) + \
datetime.timedelta(days=self.p.valid)
else:
valid = None
# Not in the market and signal to buy
if self.p.exectype == 'Market':
self.buy(exectype=bt.Order.Market) # default if not given
self.log('BUY CREATE, exectype Market, price %.2f' %
self.data.close[0])
elif self.p.exectype == 'Close':
self.buy(exectype=bt.Order.Close)
self.log('BUY CREATE, exectype Close, price %.2f' %
self.data.close[0])
elif self.p.exectype == 'Limit':
price = self.data.close * (1.0 - self.p.perc1 / 100.0)
self.buy(exectype=bt.Order.Limit, price=price, valid=valid)
if self.p.valid:
txt = 'BUY CREATE, exectype Limit, price %.2f, valid: %s'
self.log(txt % (price, valid.strftime('%Y-%m-%d')))
else:
txt = 'BUY CREATE, exectype Limit, price %.2f'
self.log(txt % price)
elif self.p.exectype == 'Stop':
price = self.data.close * (1.0 + self.p.perc1 / 100.0)
self.buy(exectype=bt.Order.Stop, price=price, valid=valid)
if self.p.valid:
txt = 'BUY CREATE, exectype Stop, price %.2f, valid: %s'
self.log(txt % (price, valid.strftime('%Y-%m-%d')))
else:
txt = 'BUY CREATE, exectype Stop, price %.2f'
self.log(txt % price)
elif self.p.exectype == 'StopLimit':
price = self.data.close * (1.0 + self.p.perc1 / 100.0)
plimit = self.data.close * (1.0 + self.p.perc2 / 100.0)
self.buy(exectype=bt.Order.StopLimit, price=price, valid=valid,
plimit=plimit)
if self.p.valid:
txt = ('BUY CREATE, exectype StopLimit, price %.2f,'
' valid: %s, pricelimit: %.2f')
self.log(txt % (price, valid.strftime('%Y-%m-%d'), plimit))
else:
txt = ('BUY CREATE, exectype StopLimit, price %.2f,'
' pricelimit: %.2f')
self.log(txt % (price, plimit))
def runstrat():
args = parse_args()
cerebro = bt.Cerebro()
data = getdata(args)
cerebro.adddata(data)
cerebro.addstrategy(
OrderExecutionStrategy,
exectype=args.exectype,
perc1=args.perc1,
perc2=args.perc2,
valid=args.valid,
smaperiod=args.smaperiod
)
cerebro.run()
if args.plot:
cerebro.plot(numfigs=args.numfigs, style=args.plotstyle)
def getdata(args):
dataformat = dict(
bt=btfeeds.BacktraderCSVData,
visualchart=btfeeds.VChartCSVData,
sierrachart=btfeeds.SierraChartCSVData,
yahoo=btfeeds.YahooFinanceCSVData,
yahoo_unreversed=btfeeds.YahooFinanceCSVData
)
dfkwargs = dict()
if args.csvformat == 'yahoo_unreversed':
dfkwargs['reverse'] = True
if args.fromdate:
fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d')
dfkwargs['fromdate'] = fromdate
if args.todate:
fromdate = datetime.datetime.strptime(args.todate, '%Y-%m-%d')
dfkwargs['todate'] = todate
dfkwargs['dataname'] = args.infile
dfcls = dataformat[args.csvformat]
return dfcls(**dfkwargs)
def parse_args():
parser = argparse.ArgumentParser(
description='Showcase for Order Execution Types')
parser.add_argument('--infile', '-i', required=False,
default='../datas/2006-day-001.txt',
help='File to be read in')
parser.add_argument('--csvformat', '-c', required=False, default='bt',
choices=['bt', 'visualchart', 'sierrachart',
'yahoo', 'yahoo_unreversed'],
help='CSV Format')
parser.add_argument('--fromdate', '-f', required=False, default=None,
help='Starting date in YYYY-MM-DD format')
parser.add_argument('--todate', '-t', required=False, default=None,
help='Ending date in YYYY-MM-DD format')
parser.add_argument('--plot', '-p', action='store_false', required=False,
help='Plot the read data')
parser.add_argument('--plotstyle', '-ps', required=False, default='bar',
choices=['bar', 'line', 'candle'],
help='Plot the read data')
parser.add_argument('--numfigs', '-n', required=False, default=1,
help='Plot using n figures')
parser.add_argument('--smaperiod', '-s', required=False, default=15,
help='Simple Moving Average Period')
parser.add_argument('--exectype', '-e', required=False, default='Market',
help=('Execution Type: Market (default), Close, Limit,'
' Stop, StopLimit'))
parser.add_argument('--valid', '-v', required=False, default=0, type=int,
help='Validity for Limit sample: default 0 days')
parser.add_argument('--perc1', '-p1', required=False, default=0.0,
type=float,
help=('%% distance from close price at order creation'
' time for the limit/trigger price in Limit/Stop'
' orders'))
parser.add_argument('--perc2', '-p2', required=False, default=0.0,
type=float,
help=('%% distance from close price at order creation'
' time for the limit price in StopLimit orders'))
return parser.parse_args()
if __name__ == '__main__':
runstrat()
扩展数据源
原文:
www.backtrader.com/blog/posts/2015-08-07-extending-a-datafeed/extending-a-datafeed/
GitHub 上的问题实际上正在推动完成文档部分或帮助我了解 backtrader 是否具有我最初设想的易用性和灵活性,并沿途做出的决定。
在这种情况下是问题#9。
最终问题似乎归结为:
- 最终用户是否可以轻松扩展现有机制,以添加额外信息,以行的形式传递到其他现有价格信息点,如
open
,high
等?
就我理解的问题,答案是:是的
发帖人似乎有这些要求(来自问题#6):
-
一个被解析为 CSV 格式的数据源
-
使用
GenericCSVData
来加载信息这种通用的 CSV 支持是为了响应这个问题#6而开发的
-
一个额外字段,显然包含需要传递到解析的 CSV 数据中的 P/E 信息
让我们在 CSV 数据源开发和通用 CSV 数据源示例帖子的基础上构建。
步骤:
-
假设 P/E 信息被设置在被解析的 CSV 数据中
-
使用
GenericCSVData
作为基类 -
将现有线(open/high/low/close/volumen/openinterest)扩展为
pe
-
添加一个参数,让调用者确定 P/E 信息的列位置
结果:
from backtrader.feeds import GenericCSVData
class GenericCSV_PE(GenericCSVData):
# Add a 'pe' line to the inherited ones from the base class
lines = ('pe',)
# openinterest in GenericCSVData has index 7 ... add 1
# add the parameter to the parameters inherited from the base class
params = (('pe', 8),)
工作完成了...
稍后,在策略中使用这个数据源时:
import backtrader as bt
....
class MyStrategy(bt.Strategy):
...
def next(self):
if self.data.close > 2000 and self.data.pe < 12:
# TORA TORA TORA --- Get off this market
self.sell(stake=1000000, price=0.01, exectype=Order.Limit)
...
绘制额外的 P/E 线
显然,在数据源中没有对该额外线的自动绘图支持。
最好的选择是对该线进行简单移动平均,并在单独的轴上绘制它:
import backtrader as bt
import backtrader.indicators as btind
....
class MyStrategy(bt.Strategy):
def __init__(self):
# The indicator autoregisters and will plot even if no obvious
# reference is kept to it in the class
btind.SMA(self.data.pe, period=1, subplot=False)
...
def next(self):
if self.data.close > 2000 and self.data.pe < 12:
# TORA TORA TORA --- Get off this market
self.sell(stake=1000000, price=0.01, exectype=Order.Limit)
...
CSV 数据源开发
原文:
www.backtrader.com/blog/posts/2015-08-06-csv-data-feed-development/csv-data-feed-development/
backtrader 已经提供了一个通用的 CSV 数据源和一些特定的 CSV 数据源。总结如下:
-
GenericCSVData
-
VisualChartCSVData
-
YahooFinanceData(用于在线下载)
-
YahooFinanceCSVData(用于已下载的数据)
-
BacktraderCSVData(内部…用于测试目的,但可以使用)
但即使如此,最终用户可能希望为特定的 CSV 数据源开发支持。
通常的格言是:“说起来容易做起来难”。实际上,结构设计得很简单。
步骤:
-
继承自
backtrader.CSVDataBase
-
如果需要,定义任何
params
-
在
start
方法中进行任何初始化 -
在
stop
方法中进行任何清理 -
定义一个
_loadline
方法,其中实际工作发生此方法接收一个参数:linetokens。
正如名称所示,这包含了在根据
separator
参数(从基类继承)分割当前行后的令牌。如果在完成其工作后有新数据... 填充相应的行并返回
True
如果没有可用的数据,因此解析已经结束:返回
False
如果在幕后读取文件行的代码发现没有更多可解析的行,则可能甚至不需要返回
False
。
已经考虑到的事项:
-
打开文件(或接收类似文件的对象)
-
如果存在,则跳过标题行
-
读取行
-
对行进行标记
-
预加载支持(一次性将整个数据源加载到内存中)
通常,一个例子胜过千言万语的要求描述。让我们使用从BacktraderCSVData
定义的简化版本的内部定义的 CSV 解析代码。这个不需要初始化或清理(例如,可以稍后打开套接字并关闭)。
注意
backtrader
数据源包含常见的行业标准数据源,这些是需要填充的。即:
-
日期时间
-
打开
-
高
-
低
-
关闭
-
成交量
-
持仓量
如果您的策略/算法或简单的数据查阅只需要,例如收盘价,您可以将其他内容保持不变(每次迭代都会在最终用户代码有机会执行任何操作之前自动使用 float('NaN')值填充它们。
在本例中仅支持每日格式:
import itertools
...
import backtrader import bt
class MyCSVData(bt.CSVDataBase):
def start(self):
# Nothing to do for this data feed type
pass
def stop(self):
# Nothing to do for this data feed type
pass
def _loadline(self, linetokens):
i = itertools.count(0)
dttxt = linetokens[next(i)]
# Format is YYYY-MM-DD
y = int(dttxt[0:4])
m = int(dttxt[5:7])
d = int(dttxt[8:10])
dt = datetime.datetime(y, m, d)
dtnum = date2num(dt)
self.lines.datetime[0] = dtnum
self.lines.open[0] = float(linetokens[next(i)])
self.lines.high[0] = float(linetokens[next(i)])
self.lines.low[0] = float(linetokens[next(i)])
self.lines.close[0] = float(linetokens[next(i)])
self.lines.volume[0] = float(linetokens[next(i)])
self.lines.openinterest[0] = float(linetokens[next(i)])
return True
代码期望所有字段都就位,并且可转换为浮点数,除了日期时间之外,它具有固定的 YYYY-MM-DD 格式,并且可以在不使用datetime.datetime.strptime
的情况下解析。
只需添加几行代码即可满足更复杂的需求,以处理空值,日期格式解析。GenericCSVData
就是这样做的。
买方注意事项
使用GenericCSVData
现有的数据源和继承可以实现很多支持格式的功能。
让我们为Sierra Chart的每日格式添加支持(该格式始终以 CSV 格式存储)。
定义(通过查看一个‘.dly’数据文件):
-
字段:日期、开盘价、最高价、最低价、收盘价、成交量、持仓量
行业标准和已由
GenericCSVData
支持的那些文件,按相同顺序(这也是行业标准) -
分隔符:,
-
日期格式:YYYY/MM/DD
针对这些文件的解析器:
class SierraChartCSVData(backtrader.feeds.GenericCSVData):
params = (('dtformat', '%Y/%m/%d'),)
params
的定义只是重新定义基类中的一个现有参数。在这种情况下,只需更改日期的格式化字符串。
哎呀……Sierra Chart 的解析器完成了。
这里是 GenericCSVData
的参数定义作为提醒:
class GenericCSVData(feed.CSVDataBase):
params = (
('nullvalue', float('NaN')),
('dtformat', '%Y-%m-%d %H:%M:%S'),
('tmformat', '%H:%M:%S'),
('datetime', 0),
('time', -1),
('open', 1),
('high', 2),
('low', 3),
('close', 4),
('volume', 5),
('openinterest', 6),
)
通用 CSV 数据源
原文:
www.backtrader.com/blog/posts/2015-08-04-generic-csv-datafeed/generic-csv-datafeed/
一个问题导致实现了GenericCSVData,可用于解析不同的 CSV 格式。
GitHub 上的问题,Issue #6清楚地显示需要有能够处理任何传入 CSV 数据源的东西。
参数声明中包含关键信息:
class GenericCSVData(feed.CSVDataBase):
params = (
('nullvalue', float('NaN')),
('dtformat', '%Y-%m-%d %H:%M:%S'),
('tmformat', '%H:%M:%S'),
('datetime', 0),
('time', -1),
('open', 1),
('high', 2),
('low', 3),
('close', 4),
('volume', 5),
('openinterest', 6),
)
因为该类继承自 CSVDataBase,一些标准参数可用:
-
fromdate
(接受日期时间对象以限制起始日期) -
todate
(接受日期时间对象)以限制结束日期) -
headers
(默认值:True,指示 CSV 数据是否有标题行) -
separator
(默认值:“,”,分隔字段的字符) -
dataname
(包含 CSV 数据的文件名或类似文件的对象)
其他一些参数如name
,compression
和timeframe
仅供参考,除非您计划执行重新采样。
当然更重要的是,新定义参数的含义:
-
datetime
(默认值:0)列包含日期(或日期时间)字段 -
time
(默认值:-1)列包含时间字段,如果与日期时间字段分开(-1 表示不存在) -
open
(默认值:1),high
(默认值:2),low
(默认值:3),close
(默认值:4),volume
(默认值:5),openinterest
(默认值:6)包含相应字段的列的索引
如果传递负值(例如:-1),表示 CSV 数据中不存在该字段
-
nullvalue
(默认值:float('NaN'))如果应该存在的值缺失(CSV 字段为空),将使用的值
-
dtformat
(默认值:%Y-%m-%d %H:%M:%S)用于解析日期时间 CSV 字段的格式
-
tmformat
(默认值:%H:%M:%S)用于解析时间 CSV 字段的格式(如果“存在”)(“时间”CSV 字段的默认值是不存在)
这可能足以涵盖许多不同的 CSV 格式和值的缺失。
涵盖以下要求的示例用法:
-
限制输入至 2000 年
-
HLOC 顺序而不是 OHLC
-
缺失值将被替换为零(0.0)
-
提供每日 K 线数据,日期时间格式为 YYYY-MM-DD
-
没有
openinterest
列存在
代码:
import datetime
import backtrader as bt
import backtrader.feeds as btfeed
...
...
data = btfeed.GenericCSVData(
dataname='mydata.csv',
fromdate=datetime.datetime(2000, 1, 1),
todate=datetime.datetime(2000, 12, 31),
nullvalue=0.0,
dtformat=('%Y-%m-%d'),
datetime=0,
high=1,
low=2,
open=3,
close=4,
volume=5,
openinterest=-1
)
稍微修改的要求:
-
限制输入至 2000 年
-
HLOC 顺序而不是 OHLC
-
缺失值将被替换为零(0.0)
-
提供分钟级 K 线数据,具有单独的日期和时间列
-
日期格式为 YYYY-MM-DD
-
时间格式为 HH.MM.SS
-
-
没有
openinterest
列存在
代码:
import datetime
import backtrader as bt
import backtrader.feeds as btfeed
...
...
data = btfeed.GenericCSVData(
dataname='mydata.csv',
fromdate=datetime.datetime(2000, 1, 1),
todate=datetime.datetime(2000, 12, 31),
nullvalue=0.0,
dtformat=('%Y-%m-%d'),
tmformat=('%H.%M.%S'),
datetime=0,
time=1,
high=2,
low=3,
open=4,
close=5,
volume=6,
openinterest=-1
)
改进佣金:股票与期货
原文:
www.backtrader.com/blog/posts/2015-07-31-commission-schemes-updated/commission-schemes-updated/
发布 backtrader 使用示例让我对一些缺失的东西有了了解。首先:
-
多核优化
-
佣金:股票与期货
后者告诉了我:
-
经纪人在利润和损失的计算方面做得很对,向调用策略提供了正确的订单通知
-
策略无法访问
operations
(又称trades
),这是订单已经开仓并关闭头寸的结果(后者显示出盈亏数字) -
绘制的
Operation
盈亏数字由一个Observer
收集,并且无法访问实际的commission scheme
,因此对于类似于期货的操作和类似于股票的操作,将显示相同的盈亏数字。
显然,需要进行一些小的内部重组才能实现:
-
向策略发送
Operation
通知 -
Operations
显示正确的盈亏数字
broker
已经拥有了所有需要的信息,并且已经将大部分信息填入了被通知到strategy
的order
中,而broker
需要做出的唯一决定是是否将额外的信息位放入订单中,或者它可以自己计算operations
。
由于策略已经获得了orders
,并且将operations
保留在列表中似乎很自然,broker
只是在订单部分/完全关闭头寸时添加实际的盈亏,将计算责任留给了strategy
。
这进一步简化了Operations Observer
的实际角色,即观察新关闭的Operation
并记录下来。这是它一直应该有的角色。
下面的代码已经被重新设计,不再计算盈亏数字,而只是注意到notify_operation
中通知的数字。
现在图表反映出了真实的盈亏数字(cash
和value
已经是真实的)
旧的期货记录:
2006-03-09, BUY CREATE, 3757.59
2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 2000.00, Comm 2.00
2006-04-11, SELL CREATE, 3788.81
2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 2000.00, Comm 2.00
2006-04-12, OPERATION PROFIT, GROSS 328.00, NET 324.00
2006-04-20, BUY CREATE, 3860.00
2006-04-21, BUY EXECUTED, Price: 3863.57, Cost: 2000.00, Comm 2.00
2006-04-28, SELL CREATE, 3839.90
2006-05-02, SELL EXECUTED, Price: 3839.24, Cost: 2000.00, Comm 2.00
2006-05-02, OPERATION PROFIT, GROSS -243.30, NET -247.30
新的期货记录:
2006-03-09, BUY CREATE, 3757.59
2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 2000.00, Comm 2.00
2006-04-11, SELL CREATE, 3788.81
2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 2000.00, Comm 2.00
2006-04-12, OPERATION PROFIT, GROSS 328.00, NET 324.00
2006-04-20, BUY CREATE, 3860.00
2006-04-21, BUY EXECUTED, Price: 3863.57, Cost: 2000.00, Comm 2.00
2006-04-28, SELL CREATE, 3839.90
2006-05-02, SELL EXECUTED, Price: 3839.24, Cost: 2000.00, Comm 2.00
2006-05-02, OPERATION PROFIT, GROSS -243.30, NET -247.30
2006-05-02, BUY CREATE, 3862.24
旧的股票记录:
2006-03-09, BUY CREATE, 3757.59
2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 3754.13, Comm 18.77
2006-04-11, SELL CREATE, 3788.81
2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 3786.93, Comm 18.93
2006-04-12, OPERATION PROFIT, GROSS 32.80, NET -4.91
2006-04-20, BUY CREATE, 3860.00
2006-04-21, BUY EXECUTED, Price: 3863.57, Cost: 3863.57, Comm 19.32
2006-04-28, SELL CREATE, 3839.90
2006-05-02, SELL EXECUTED, Price: 3839.24, Cost: 3839.24, Comm 19.20
2006-05-02, OPERATION PROFIT, GROSS -24.33, NET -62.84
新的股票记录:
2006-03-09, BUY CREATE, 3757.59
2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 3754.13, Comm 18.77
2006-04-11, SELL CREATE, 3788.81
2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 3786.93, Comm 18.93
2006-04-12, OPERATION PROFIT, GROSS 32.80, NET -4.91
2006-04-20, BUY CREATE, 3860.00
2006-04-21, BUY EXECUTED, Price: 3863.57, Cost: 3863.57, Comm 19.32
2006-04-28, SELL CREATE, 3839.90
2006-05-02, SELL EXECUTED, Price: 3839.24, Cost: 3839.24, Comm 19.20
2006-05-02, OPERATION PROFIT, GROSS -24.33, NET -62.84
2006-05-02, BUY CREATE, 3862.24
图表(仅新的图表)。现在可以清楚地看到futures-like
操作和stock-like
操作之间的差异,不仅在cash
和value
的变化中。
期货佣金
股票佣金
代码
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import backtrader as bt
import backtrader.feeds as btfeeds
import backtrader.indicators as btind
futures_like = True
if futures_like:
commission, margin, mult = 2.0, 2000.0, 10.0
else:
commission, margin, mult = 0.005, None, 1
class SMACrossOver(bt.Strategy):
def log(self, txt, dt=None):
''' Logging function fot this strategy'''
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def notify(self, order):
if order.status in [order.Submitted, order.Accepted]:
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
return
# Check if an order has been completed
# Attention: broker could reject order if not enougth cash
if order.status in [order.Completed, order.Canceled, order.Margin]:
if order.isbuy():
self.log(
'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
else: # Sell
self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
def notify_trade(self, trade):
if trade.isclosed:
self.log('TRADE PROFIT, GROSS %.2f, NET %.2f' %
(trade.pnl, trade.pnlcomm))
def __init__(self):
sma = btind.SMA(self.data)
# > 0 crossing up / < 0 crossing down
self.buysell_sig = btind.CrossOver(self.data, sma)
def next(self):
if self.buysell_sig > 0:
self.log('BUY CREATE, %.2f' % self.data.close[0])
self.buy() # keep order ref to avoid 2nd orders
elif self.position and self.buysell_sig < 0:
self.log('SELL CREATE, %.2f' % self.data.close[0])
self.sell()
if __name__ == '__main__':
# Create a cerebro entity
cerebro = bt.Cerebro()
# Add a strategy
cerebro.addstrategy(SMACrossOver)
# Create a Data Feed
datapath = ('../../datas/2006-day-001.txt')
data = bt.feeds.BacktraderCSVData(dataname=datapath)
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# set commission scheme -- CHANGE HERE TO PLAY
cerebro.broker.setcommission(
commission=commission, margin=margin, mult=mult)
# Run over everything
cerebro.run()
# Plot the result
cerebro.plot()
佣金:股票 vs 期货
原文:
www.backtrader.com/blog/posts/2015-07-26-commission-schemes/commission-schemes/
backtrader 的诞生是出于必要性。我自己...希望有一种感觉,我可以控制自己的回测平台并尝试新的想法。但是在这样做并从一开始就完全开源化之后,很明显它必须有一种方式来满足其他人的需求和愿望。
作为交易者未来,我本可以选择编写基于点数的计算和每轮固定价格的佣金,但那将是一个错误。
注意
2015 年 7 月 31 日
跟进帖子,附带新添加的操作/交易通知,修复交易 P&L 图表的绘制,并避免像下面示例中那样的手动计算。
改善佣金:股票 vs 期货
相反,backtrader
提供了使用常规%大小/价格基础方案和固定价格/点方案的可能性。选择权在你手上。
不可知论
在继续之前,让我们记住`backtrader`试图保持对数据表示的不可知。可以将不同的佣金方案应用于相同的数据集。
让我们看看如何做到这一点。
## 使用经纪人快捷方式
这样可以使最终用户远离`CommissionInfo`对象,因为可以通过单个函数调用创建/设置佣金方案。在常规的`cerebro`创建/设置过程中,只需将调用添加到`broker`成员变量上即可。以下调用在使用*InteractiveBrokers*时为**Eurostoxx50**期货设置了一种常规佣金方案:
cerebro.broker.setcommission(commission=2.0, margin=2000.0, mult=10.0)
由于大多数用户通常只测试单个工具,因此这就是问题的全部。如果您已经为数据源指定了name
,因为图表上同时考虑了多个工具,因此此调用可以略微扩展为如下所示:
cerebro.broker.setcommission(commission=2.0, margin=2000.0, mult=10.0,
name='Eurostoxxx50')
在这种情况下,这种即时佣金方案将仅应用于名称与Eurostoxx50
匹配的工具。
设置佣金参数的含义
-
commission
(默认值:0.0)每个操作的货币单位以绝对值或百分比形式的成本。
在上面的示例中,每个
buy
合约需要 2.0 欧元,每个sell
合约也是如此。这里的重要问题是何时使用绝对值或百分比值。
-
如果
margin
评估为False
(例如为 False、0 或 None),则将认为commission
表示price
乘以size
操作值的百分比。 -
如果
margin
是其他内容,则认为操作发生在类似期货
的工具上,并且commission
是每个size
合约的固定价格
-
-
margin
(默认值:None)使用
期货
等工具时需要的保证金。如上所述-
如果设置了no
margin
,则commission
将被理解为以百分比表示,并应用于buy
或sell
操作的price * size
组件 -
如果设置了
margin
,则commission
将被理解为与buy
或sell
操作的size
分量相乘的固定值
-
-
mult
(默认:1.0)对于
类似期货
的工具,这决定了要应用于利润和损失计算的乘数。这就是期货同时具有吸引力和风险的原因。
-
name
(默认:无)限制佣金方案的应用于与
name
匹配的工具。这可以在创建数据源时设置。
如果不设置,方案将应用于系统中存在的任何数据。
现在有两个例子:股票 vs 期货
来自上述的期货示例:
cerebro.broker.setcommission(commission=2.0, margin=2000.0, mult=10.0)
股票的一个例子:
cerebro.broker.setcommission(commission=0.005) # 0.5% of the operation value
创建永久佣金方案
更持久的佣金方案可以通过直接使用CommissionInfo
类来创建。用户可以选择将此定义放在某处:
from bt import CommissionInfo
commEurostoxx50 = CommissionInfo(commission=2.0, margin=2000.0, mult=10.0)
然后在另一个 Python 模块中应用它与addcommissioninfo
:
from mycomm import commEurostoxx50
...
cerebro.broker.addcomissioninfo(commEuroStoxx50, name='Eurostoxxx50')
CommissionInfo
是一个对象,它使用与backtrader
环境中的其他对象一样的params
声明。因此,上述内容也可以表示为:
from bt import CommissionInfo
class CommEurostoxx50(CommissionInfo):
params = dict(commission=2.0, margin=2000.0, mult=10.0)
后来:
from mycomm import CommEurostoxx50
...
cerebro.broker.addcomissioninfoCommEuroStoxx50(), name='Eurostoxxx50')
现在是与 SMA 交叉的“真实”比较
使用 SimpleMovingAverage 交叉作为入场/出场信号,将使用类似期货
的佣金方案对同一数据集进行测试,然后再使用类似股票
的方案。
注意
期货头寸不仅可以在每次发生时赋予进入/退出行为,还可以在每次发生时赋予反转行为。但是,此示例是关于比较佣金方案的。
代码(请参阅底部获取完整策略)是相同的,可以在定义策略之前选择方案。
futures_like = True
if futures_like:
commission, margin, mult = 2.0, 2000.0, 10.0
else:
commission, margin, mult = 0.005, None, 1
只需将futures_like
设置为 false 即可使用类似股票
的方案运行。
已添加一些记录代码以评估不同佣金方案的影响。让我们只关注前两个操作。
对于期货:
2006-03-09, BUY CREATE, 3757.59
2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 2000.00, Comm 2.00
2006-04-11, SELL CREATE, 3788.81
2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 2000.00, Comm 2.00
2006-04-12, OPERATION PROFIT, GROSS 328.00, NET 324.00
2006-04-20, BUY CREATE, 3860.00
2006-04-21, BUY EXECUTED, Price: 3863.57, Cost: 2000.00, Comm 2.00
2006-04-28, SELL CREATE, 3839.90
2006-05-02, SELL EXECUTED, Price: 3839.24, Cost: 2000.00, Comm 2.00
2006-05-02, OPERATION PROFIT, GROSS -243.30, NET -247.30
对于股票:
2006-03-09, BUY CREATE, 3757.59
2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 3754.13, Comm 18.77
2006-04-11, SELL CREATE, 3788.81
2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 3786.93, Comm 18.93
2006-04-12, OPERATION PROFIT, GROSS 32.80, NET -4.91
2006-04-20, BUY CREATE, 3860.00
2006-04-21, BUY EXECUTED, Price: 3863.57, Cost: 3863.57, Comm 19.32
2006-04-28, SELL CREATE, 3839.90
2006-05-02, SELL EXECUTED, Price: 3839.24, Cost: 3839.24, Comm 19.20
2006-05-02, OPERATION PROFIT, GROSS -24.33, NET -62.84
第一次操作具有以下价格:
-
买入(执行)-> 3754.13 / 卖出(执行)-> 3786.93
-
期货利润和损失(含佣金):324.0
-
股票利润和损失(含佣金):-4.91
嘿!! 佣金完全吞噬了
股票
操作的任何利润,但对期货
操作只是造成了小小的凹痕。 -
第二次操作:
-
买入(执行)-> 3863.57 / 卖出(执行)-> 3389.24
-
期货利润和损失(含佣金):-247.30
-
股票利润和损失(含佣金):-62.84
这次负面操作对于
期货
的咬度明显更大 -
但:
-
期货累计净利润和损失:324.00 + (-247.30) = 76.70
-
股票累计净利润和损失:(-4.91) + (-62.84) = -67.75
累计效果可以在下面的图表中看到,在完整年份结束时,期货产生了更大的利润,但也遭受了更大的回撤(深入水中更深)
但重要的是:无论是期货
还是股票
… 都可以进行回测。
期货佣金
股票佣金
代码
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import backtrader as bt
import backtrader.feeds as btfeeds
import backtrader.indicators as btind
futures_like = True
if futures_like:
commission, margin, mult = 2.0, 2000.0, 10.0
else:
commission, margin, mult = 0.005, None, 1
class SMACrossOver(bt.Strategy):
def log(self, txt, dt=None):
''' Logging function fot this strategy'''
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def notify(self, order):
if order.status in [order.Submitted, order.Accepted]:
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
return
# Check if an order has been completed
# Attention: broker could reject order if not enougth cash
if order.status in [order.Completed, order.Canceled, order.Margin]:
if order.isbuy():
self.log(
'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
self.opsize = order.executed.size
else: # Sell
self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
gross_pnl = (order.executed.price - self.buyprice) * \
self.opsize
if margin:
gross_pnl *= mult
net_pnl = gross_pnl - self.buycomm - order.executed.comm
self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
(gross_pnl, net_pnl))
def __init__(self):
sma = btind.SMA(self.data)
# > 0 crossing up / < 0 crossing down
self.buysell_sig = btind.CrossOver(self.data, sma)
def next(self):
if self.buysell_sig > 0:
self.log('BUY CREATE, %.2f' % self.data.close[0])
self.buy() # keep order ref to avoid 2nd orders
elif self.position and self.buysell_sig < 0:
self.log('SELL CREATE, %.2f' % self.data.close[0])
self.sell()
if __name__ == '__main__':
# Create a cerebro entity
cerebro = bt.Cerebro()
# Add a strategy
cerebro.addstrategy(SMACrossOver)
# Create a Data Feed
datapath = ('../datas/2006-day-001.txt')
data = bt.feeds.BacktraderCSVData(dataname=datapath)
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# set commission scheme -- CHANGE HERE TO PLAY
cerebro.broker.setcommission(
commission=commission, margin=margin, mult=mult)
# Run over everything
cerebro.run()
# Plot the result
cerebro.plot()
多核优化
www.backtrader.com/blog/posts/2015-07-23-multicore-optimization/multicore-optimization/
利用所有可用核心是我对 backtrader 有的想法,但从未实现。支持自然操作,移除数组表示法,包含新的指标等等。
实际上,我并不是优化的忠实粉丝,因此对于为此利用所有核心也不是忠实粉丝。在我看来,一个好主意值得百万次优化。
注意
初始的多核支持已经存在,并且对于众所周知的一组测试用例有效。鉴于pickle
所展示的行为,预计还需要进行一些其他调整,以确保在进行多核优化时可以在进程之间传递所有指标和函数。
注意
针对多核的一些额外校正已经发布为 1.0.10.88 版本,以使更多的“不可序列化”项变得可序列化。到目前为止,指标测试没有出现任何问题。
但是 BigMikeTrading 论坛上有人询问这个平台相比其他平台有什么优势,我提到了一些功能,包括PyAlgoTrade,例如,已经有了(甚至是多机器的)。
这需要做一点小而正确的推动。根据过去的经验以及因为互联网上充满了参考资料,我已经知道:多线程即使是最简单的(无论 GIL 律师们可能说什么),在 Python 中也是行不通的,无论版本如何。在 Python 中,多线程是假的,因为你有多个线程,但没有代码的并行执行。在 Python 中使用多线程可能会创建抽象,并用 IO 绑定的线程分开代码路径的执行,但这确实是一个致命问题。
那么我只剩下一个选择:模块multiprocessing
或类似的模块。
展望光明的未来,我决定选择现代版本:concurrent.futures
(后来证明是一个错误的选择)。即使这意味着为 Python 2.6/2.7 支持添加外部依赖。
历史:
-
Python 的一些动态特性与在进程之间发送数据不兼容
-
当序列化一些像类不在模块级别定义、lambda 表达式、对实例方法的引用以及没有唯一名称的动态类(即使类本身是唯一的)时,所涉及的模块(
pickle
)会出错。
我把这些东西散落在代码中。然后我发现了dill和 pathos 多进程的兄弟姐妹pypi.python.org/pypi/multiprocess
。显然它们可以解决序列化问题,但是添加更多的外部依赖……不行不行。
回到起点,看看那些不可序列化的项是否可以被序列化,即使pickle
模块产生了一些错误,这将使一些旧的 GCC 开发人员非常高兴。
它完成了吗……还是没有?
-
将不可选的项目改造为可选项目
-
用 Python 2.7.9 进行测试,并像风一样轻松地运行……我的机器的 8 个核心顺畅且令人耳目一新
-
使用 Python 3.4.3 进行测试,8 个核心开始运作,但在进行一些优化后,每个后续策略的执行时间会越来越长……直到不堪忍受为止。
显然,将结果(完整的执行策略)反向 pickling 到主进程中触及了一些与内存分配相关的限制(我的机器有大量空闲 RAM……足够多以进行几小时的并行优化)
阅读了一些额外的内容后,我考虑简化我的情景:
-
使用
concurrent.futures
看起来更具未来性 -
但标准的
multiprocessing
模块已经具备了backtrader
所需的功能
闻起来好像有点过度,一些行被迅速改写成:
-
测试用 Python 2.7 运行正常(甚至比以前更快)
-
测试用 Python 3.4 同样快速运行
进行清理,运行完整的一系列测试并执行推送,发布 1.0.9.88。没有新的指标……只是多核优化的普通旧方式
读完这些……是时候写一个关于如何控制优化以使用多个核心的清爽脚本了
- 好消息……不需要做任何事情……它在用户不介入的情况下完成了
当用户希望优化 strategy
时,Strategy
子类将被添加到 Cerebro
实例中,如下所示:
cerebro.optstrategy(StrategyClass, *args, **kwargs)
与向 Cerebro
传递策略的常规方式相反:
cerebro.addstrategy(StrategyClass, *args, **kwargs)
这一直都是这样,没有改变。背景是:
Cerebro
需要了解是否要优化策略,以正确处理可能已经是常规策略的策略的参数
现在……通过 optstrategy
传递给 cerebro
的策略将获得使用机器上所有可用核心的额外好处。
当然,如果最终用户希望对使用的核心进行精细控制……是可能的。创建 Cerebro
的标准方式:
cerebro = bt.Cerebro() # runonce 为 True,preload 为 True,且 “new” maxcpus 为 None
maxcpus
(此版本中的新参数)是控制键:
-
maxcpus = None -> 使用所有可用的 CPU
-
maxcpus = 1 -> 不要运行多核
-
maxcpues = 2 … -> 使用指定数量的核心
这是一种选择退出策略,因为多核已经存在。
在拥有 16 GBytes RAM 的 4 核(每核 2 个线程 - 总共 8 个逻辑处理器)机器上进行比较,运行 Windows 8.1 和 Python 64 位 2.7.9
-
使用 1 个核心执行:326 秒
-
使用 8 个核心执行:127 秒
不同的测试运行显示,平均比例约为 2.75:1。
不幸的是,进程的创建/销毁和对象的反复 pickling 带来了潜在的好处,但加速效果仍然显著。
图像显示了正在使用的 8 个核心。
代码如下。只需将maxcpus
参数的1
更改为限制测试为 1 个核心。
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import time
from six.moves import xrange
import backtrader as bt
import backtrader.indicators as btind
import backtrader.feeds as btfeeds
class OptimizeStrategy(bt.Strategy):
params = (('smaperiod', 15),
('macdperiod1', 12),
('macdperiod2', 26),
('macdperiod3', 9),
)
def __init__(self):
# Add indicators to add load
btind.SMA(period=self.p.smaperiod)
btind.MACD(period_me1=self.p.macdperiod1,
period_me2=self.p.macdperiod2,
period_signal=self.p.macdperiod3)
if __name__ == '__main__':
# Create a cerebro entity
cerebro = bt.Cerebro(maxcpus=None)
# Add a strategy
cerebro.optstrategy(
OptimizeStrategy,
smaperiod=xrange(5, 40),
macdperiod1=xrange(12, 20),
macdperiod2=xrange(26, 30),
macdperiod3=xrange(9, 15),
)
# Create a Data Feed
datapath = ('../datas/2006-day-001.txt')
data = bt.feeds.BacktraderCSVData(dataname=datapath)
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# clock the start of the process
tstart = time.clock()
# Run over everything
stratruns = cerebro.run()
# clock the end of the process
tend = time.clock()
print('==================================================')
for stratrun in stratruns:
print('**************************************************')
for strat in stratrun:
print('--------------------------------------------------')
print(strat.p._getkwargs())
print('==================================================')
# print out the result
print('Time used:', str(tend - tstart))
扩展一个指标
原文:
www.backtrader.com/blog/posts/2015-07-20-extending-an-indicator/extending-an-indicator/
在面向对象编程中,当然也包括 Python 本身,对现有类的扩展可以通过两种方式实现。
-
继承(或子类化)
-
组合(或嵌入)
在开发一个指标时,指标Trix
只需几行代码就可以开发完成。ChartSchool - Trix 参考文献中有一个带有信号线的Trix
,显示了与 MACD 的相似之处。
让我们使用已经开发的Trix
“组合”MyTrixSignal
class MyTrixSignalComposed(bt.Indicator):
lines = ('trix', 'signal')
params = (('period', 15), ('sigperiod', 9))
def __init__(self):
self.lines.trix = MyTrix(self.data, period=self.p.period)
self.lines.signal = btind.EMA(self.lines.trix, period=self.p.sigperiod)
在定义中有一些必须重复的内容,比如trix
线的名称和用于计算的period
。定义了一个新的signal
线和相应的sigperiod
参数。
这个两行的结果很好。
现在让我们来看看继承,但首先回顾一下Trix
的样子:
class MyTrix(bt.Indicator):
lines = ('trix',)
params = (('period', 15),)
def __init__(self):
ema1 = btind.EMA(self.data, period=self.p.period)
ema2 = btind.EMA(ema1, period=self.p.period)
ema3 = btind.EMA(ema2, period=self.p.period)
self.lines.trix = 100.0 * (ema3 - ema3(-1)) / ema3(-1)
使用Trix
作为基类,这是TrixSignal
的外观
class MyTrixSignalInherited(MyTrix):
lines = ('signal',)
params = (('sigperiod', 9),)
def __init__(self):
super(MyTrixSignalInherited, self).__init__()
self.lines.signal = btind.EMA(self.lines.trix, period=self.p.sigperiod)
继承的指标最终也是一个两行代码,但是:
-
不需要重新定义
trix
线 -
不需要重新定义
period
参数
两者都是从基类Trix
继承而来。trix
线的计算是在基类__init__
方法中完成的:
- super(MyTrixSignalInherited, self).init()
组合与继承的选择是一个经典问题。这个例子并不是为了澄清哪种更好,而更多是为了展示:
注意
即使存在lines和params的元定义,它们也继承自基类的元定义
最后是代码和图表,展示两个版本的运行情况。
- 第一个展示了继承版本
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import backtrader as bt
import backtrader.feeds as btfeeds
from mytrix import MyTrixSignalInherited
class NoStrategy(bt.Strategy):
params = (('trixperiod', 15),
('analyzer', False),)
def __init__(self):
MyTrixSignalInherited(self.data, period=self.p.trixperiod)
if __name__ == '__main__':
# Create a cerebro entity
cerebro = bt.Cerebro()
# Add a strategy
cerebro.addstrategy(NoStrategy, trixperiod=15)
# Create a Data Feed
datapath = ('../datas/2006-day-001.txt')
data = bt.feeds.BacktraderCSVData(dataname=datapath)
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Run over everything
cerebro.run()
# Plot the result
cerebro.plot()
- 第一个展示了组合版本
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import backtrader as bt
import backtrader.feeds as btfeeds
from mytrix import MyTrixSignalComposed
class NoStrategy(bt.Strategy):
params = (('trixperiod', 15),
('analyzer', False),)
def __init__(self):
MyTrixSignalComposed(self.data, period=self.p.trixperiod)
if __name__ == '__main__':
# Create a cerebro entity
cerebro = bt.Cerebro()
# Add a strategy
cerebro.addstrategy(NoStrategy, trixperiod=15)
# Create a Data Feed
datapath = ('../datas/2006-day-001.txt')
data = bt.feeds.BacktraderCSVData(dataname=datapath)
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Run over everything
cerebro.run()
# Plot the result
cerebro.plot()
标签:00,59,二十八,self,BackTrader,order,文档,2006,price
From: https://www.cnblogs.com/apachecn/p/18135567