Bid-Ask 数据到 OHLC
原文:
www.backtrader.com/blog/posts/2016-04-14-bidask-data-to-ohlc/bidask-data-to-ohlc/
最近,backtrader 通过实现线覆盖来执行了从 ohlcland 的逃逸,这允许重新定义整个层次结构,例如拥有仅包含 bid、ask 和 datetime 行的数据源。
(这里是原始 Escape from OHLC Land)
这引发了如何可视化此类数据的问题,这在OHLC
格式中最有效地完成(无论是bar
还是candlestick
)
所需步骤:
- 定义一个可以读取给定
bid/ask
格式的数据源加载器 - 决定将值分配给哪些字段,即:
open
、high
、low
和close
(也许还有volume
) - 决定一个重新采样方案
源数据(10 行 bid-ask 数据):
Date,Time,Symbol,Status,Bid,Ask,Bid Vol,Ask Vol 01/03/16,23:43:11,EUR/JPY,D,,130.520,,1000000 01/03/16,23:43:27,EUR/JPY,D,,130.520,,2000000 01/03/16,23:49:19,EUR/JPY,D,,130.510,,500000 01/03/16,23:49:22,EUR/JPY,D,,130.530,,1500000 01/03/16,23:49:25,EUR/JPY,D,,130.540,,750000 01/03/16,23:49:27,EUR/JPY,D,,130.550,,900000 01/03/16,23:51:25,EUR/JPY,D,,130.500,,1200000 01/03/16,23:52:27,EUR/JPY,D,,130.495,,1100000 01/03/16,23:53:25,EUR/JPY,D,,130.480,,600000 01/03/16,23:54:27,EUR/JPY,D,,130.470,,900000
之后:
- 阅读数据不会是一个主要问题,因为最终结果必须是 OHLC,这是内置数据源在解析后提供的内容。因为这是另一种csv的变体。我们甚至可以重用现有的
GenericCSVData
数据源。感谢上帝,它是通用的 - 只有单个价格元素和单个成交量元素的情况下,价格分配是清晰的:将价格分配给四个价格元素,将成交量分配给成交量
- 当涉及到重新采样时,与上采样到更大时间框架不同,关键将是条数,即:compression
而内置的重采样器已经可以提供相同的timeframe但是压缩了。
使用GenericCSVData
将数据转换为 OHLC 格式:
data = btfeeds.GenericCSVData( dataname=args.data, dtformat='%d/%m/%y', # tmformat='%H%M%S', # already the default value # datetime=0, # position at default time=1, # position of time open=5, # position of open high=5, low=5, close=5, volume=7, openinterest=-1, # -1 for not present timeframe=bt.TimeFrame.Ticks)
一些参数甚至不需要更改,即:
tmformat
:因为数据源中的时间已经与默认格式匹配datetime
:因为日期在 csv 流中的第一个位置
其他:
time=1
:指示时间不在单个字段中与date
一起,并指定其位置open=5
(对于high
、low
、close
也是一样):流中哪个字段将用作价格的源volume=7
:与上述相同openinterest=-1
:负值表示此字段不存在
一旦数据准备就绪,就只需对其进行重新采样:
cerebro.resampledata(data, timeframe=bt.TimeFrame.Ticks, compression=args.compression)
我们提供相同的timeframe
,数据携带的是TimeFrame.Ticks
,以确保数据不被上采样。而compression
是从命令行中传递的参数,因此:compression=args.compression
一个示例执行:
$ ./bidask-to-ohlc.py --compression 2 2016-03-01 23:43:27,130.52,130.52,130.52,130.52,3000000.0 2016-03-01 23:49:22,130.51,130.53,130.53,130.53,2000000.0 2016-03-01 23:49:27,130.54,130.55,130.55,130.55,1650000.0 2016-03-01 23:52:27,130.5,130.5,130.5,130.495,2300000.0 2016-03-01 23:54:27,130.48,130.48,130.48,130.47,1500000.0
不出所料,我们已经从Bid/Ask格式转换为OHLC格式,并且由于分配给压缩的2
,数据已经从10
行减少到5
行。
也不应该让人惊讶,backtrader
不能创造奇迹,如果compression因子不是原始行数的除数,它将传递rows / compression + 1
行新行:
$ ./bidask-to-ohlc.py --compression 3 2016-03-01 23:49:19,130.52,130.52,130.52,130.51,3500000.0 2016-03-01 23:49:27,130.53,130.55,130.55,130.55,3150000.0 2016-03-01 23:53:25,130.5,130.5,130.5,130.48,2900000.0 2016-03-01 23:54:27,130.47,130.47,130.47,130.47,900000.0
在这种情况下,10 / 3 = 3.33333
,这就是为什么会传递4
行的原因。
当然,现在手中有了 OHLC
数据,结果可以绘制出来。由于数据量少且数据变化小以及 matplotlib
内部处理此情况的方式,图表看起来并不怎么好看。
样例代码(包含在 backtrader
的源代码中)
from __future__ import (absolute_import, division, print_function,) # unicode_literals) import argparse import datetime import backtrader as bt import backtrader.feeds as btfeeds class St(bt.Strategy): def next(self): print(','.join(str(x) for x in [ self.data.datetime.datetime(), self.data.open[0], self.data.high[0], self.data.high[0], self.data.close[0], self.data.volume[0]])) def runstrat(): args = parse_args() cerebro = bt.Cerebro() data = btfeeds.GenericCSVData( dataname=args.data, dtformat='%d/%m/%y', # tmformat='%H%M%S', # already the default value # datetime=0, # position at default time=1, # position of time open=5, # position of open high=5, low=5, close=5, volume=7, openinterest=-1, # -1 for not present timeframe=bt.TimeFrame.Ticks) cerebro.resampledata(data, timeframe=bt.TimeFrame.Ticks, compression=args.compression) cerebro.addstrategy(St) cerebro.run() if args.plot: cerebro.plot(style='bar') def parse_args(): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='BidAsk to OHLC') parser.add_argument('--data', required=False, default='../../datas/bidask2.csv', help='Data file to be read in') parser.add_argument('--compression', required=False, default=2, type=int, help='How much to compress the bars') parser.add_argument('--plot', required=False, action='store_true', help='Plot the vars') return parser.parse_args() if __name__ == '__main__': runstrat()
逃离 OHLC 之地
原文:
www.backtrader.com/blog/posts/2016-03-08-escape-from-ohlc-land/escape-from-ohlc-land/
在 backtrader 的构思和开发过程中应用的一个关键概念是灵活性。Python 的元编程和内省能力是保持许多灵活性的基础,同时仍能够交付成果。
一篇旧文章展示了扩展概念。
基础知识:
from backtrader.feeds import GenericCSVData class GenericCSV_PE(GenericCSVData): lines = ('pe',) # Add 'pe' to already defined lines
完成。backtrader
在后台定义了最常见的线:OHLC。
如果我们深入研究GenericCSV_PE
的最终方面,继承加上新定义的线的总和将产生以下线:
('close', 'open', 'high', 'low', 'volume', 'openinterest', 'datetime', 'pe',)
这可以随时通过getlinealiases
方法检查(适用于DataFeeds、Indicators、Strategies和Observers)
机制是灵活的,通过对内部进行一些探索,你实际上可以得到任何东西,但已经证明这还不够。
Ticket #60询问是否支持高频数据,即:Bid/Ask 数据。这意味着以OHLC形式预定义的lines层次结构不够用。Bid和Ask价格、成交量和交易数量可以适应现有的OHLC字段,但这不会感觉自然。如果只关注Bid和Ask价格,会有太多未触及的字段。
这需要一个解决方案,已经在发布 1.2.1.88中实施。这个想法可以总结为:
- 现在不仅可以扩展现有的层次结构,还可以用新的层次结构替换原有的层次结构
只有一个约束条件:
- 必须存在一个
datetime
字段(希望其中包含有意义的datetime
信息)
这是因为backtrader
需要一些用于同步的东西(多个数据源、多个时间框架、重新采样、重播),就像阿基米德需要杠杆一样。
这是它的工作原理:
from backtrader.feeds import GenericCSVData class GenericCSV_BidAsk(GenericCSVData): linesoverride = True lines = ('bid', 'ask', 'datetime') # Replace hierarchy with this one
完成。
好的,不完全是因为我们正在查看从csv源加载的行。层次结构实际上已经被替换为bid, ask datetime定义,这要归功于linesoverride=True
设置。
原始的GenericCSVData
类解析一个csv文件,并需要提示哪里是对应于lines的fields。原始定义如下:
class GenericCSVData(feed.CSVDataBase): params = ( ('nullvalue', float('NaN')), ('dtformat', '%Y-%m-%d %H:%M:%S'), ('tmformat', '%H:%M:%S'), ('datetime', 0), ('time', -1), # -1 means not present ('open', 1), ('high', 2), ('low', 3), ('close', 4), ('volume', 5), ('openinterest', 6), )
新的重新定义层次结构类可以轻松完成:
from backtrader.feeds import GenericCSVData class GenericCSV_BidAsk(GenericCSVData): linesoverride = True lines = ('bid', 'ask', 'datetime') # Replace hierarchy with this one params = (('bid', 1), ('ask', 2))
表明Bid价格是 csv 流中的字段#1,Ask价格是字段#2。我们保留了基类中的datetime #0 定义不变。
为此制作一个小数据文件有所帮助:
TIMESTAMP,BID,ASK 02/03/2010 16:53:50,0.5346,0.5347 02/03/2010 16:53:51,0.5343,0.5347 02/03/2010 16:53:52,0.5543,0.5545 02/03/2010 16:53:53,0.5342,0.5344 02/03/2010 16:53:54,0.5245,0.5464 02/03/2010 16:53:54,0.5460,0.5470 02/03/2010 16:53:56,0.5824,0.5826 02/03/2010 16:53:57,0.5371,0.5374 02/03/2010 16:53:58,0.5793,0.5794 02/03/2010 16:53:59,0.5684,0.5688
将一个小测试脚本添加到等式中(对于那些直接转到源代码中的示例的人来说,增加一些内容)(见最后的完整代码):
$ ./bidask.py
输出说明一切:
1: 2010-02-03T16:53:50 - Bid 0.5346 - 0.5347 Ask 2: 2010-02-03T16:53:51 - Bid 0.5343 - 0.5347 Ask 3: 2010-02-03T16:53:52 - Bid 0.5543 - 0.5545 Ask 4: 2010-02-03T16:53:53 - Bid 0.5342 - 0.5344 Ask 5: 2010-02-03T16:53:54 - Bid 0.5245 - 0.5464 Ask 6: 2010-02-03T16:53:54 - Bid 0.5460 - 0.5470 Ask 7: 2010-02-03T16:53:56 - Bid 0.5824 - 0.5826 Ask 8: 2010-02-03T16:53:57 - Bid 0.5371 - 0.5374 Ask 9: 2010-02-03T16:53:58 - Bid 0.5793 - 0.5794 Ask 10: 2010-02-03T16:53:59 - Bid 0.5684 - 0.5688 Ask
瞧!Bid/Ask价格已经被正确读取、解析和解释,并且策略已能通过self.data访问数据源中的*.bid和.ask*行。
重新定义lines层次结构却带来一个广泛的问题,即已预定义的Indicators的用法。
- 例如:Stochastic是一种依赖于close、high和low价格来计算其输出的指标。
即使我们把Bid视为close(因为它是第一个),仅有另一个price元素(Ask),而不是另外两个。而概念上,Ask与high和low没有任何关系。
有可能,与这些字段一起工作并在高频交易领域操作(或研究)的人并不关心Stochastic作为首选指标 - 其他指标如移动平均完全正常。它们对字段的含义或暗示不做任何假设,且乐意接受任何东西。因此,可以这样做:
mysma = backtrader.indicators.SMA(self.data.bid, period=5)`
- 并且将提供最后 5 个bid价格的移动平均线
测试脚本已经支持添加SMA。让我们执行:
$ ./bidask.py --sma --period=3
输出:
3: 2010-02-03T16:53:52 - Bid 0.5543 - 0.5545 Ask - SMA: 0.5411 4: 2010-02-03T16:53:53 - Bid 0.5342 - 0.5344 Ask - SMA: 0.5409 5: 2010-02-03T16:53:54 - Bid 0.5245 - 0.5464 Ask - SMA: 0.5377 6: 2010-02-03T16:53:54 - Bid 0.5460 - 0.5470 Ask - SMA: 0.5349 7: 2010-02-03T16:53:56 - Bid 0.5824 - 0.5826 Ask - SMA: 0.5510 8: 2010-02-03T16:53:57 - Bid 0.5371 - 0.5374 Ask - SMA: 0.5552 9: 2010-02-03T16:53:58 - Bid 0.5793 - 0.5794 Ask - SMA: 0.5663 10: 2010-02-03T16:53:59 - Bid 0.5684 - 0.5688 Ask - SMA: 0.5616
注意
绘图仍然依赖于open
、high
、low
、close
和volume
存在于数据源中。
一些情况可以通过简单地用Line on Close绘图并仅取对象中的第 1 个定义行来直接覆盖。但是必须开发一个合理的模型。针对即将推出的backtrader
版本
测试脚本用法:
$ ./bidask.py --help usage: bidask.py [-h] [--data DATA] [--dtformat DTFORMAT] [--sma] [--period PERIOD] Bid/Ask Line Hierarchy optional arguments: -h, --help show this help message and exit --data DATA, -d DATA data to add to the system (default: ../../datas/bidask.csv) --dtformat DTFORMAT, -dt DTFORMAT Format of datetime in input (default: %m/%d/%Y %H:%M:%S) --sma, -s Add an SMA to the mix (default: False) --period PERIOD, -p PERIOD Period for the sma (default: 5)
而测试脚本本身(包含在backtrader
源代码中)
from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind class BidAskCSV(btfeeds.GenericCSVData): linesoverride = True # discard usual OHLC structure # datetime must be present and last lines = ('bid', 'ask', 'datetime') # datetime (always 1st) and then the desired order for params = ( # (datetime, 0), # inherited from parent class ('bid', 1), # default field pos 1 ('ask', 2), # default field pos 2 ) class St(bt.Strategy): params = (('sma', False), ('period', 3)) def __init__(self): if self.p.sma: self.sma = btind.SMA(self.data, period=self.p.period) def next(self): dtstr = self.data.datetime.datetime().isoformat() txt = '%4d: %s - Bid %.4f - %.4f Ask' % ( (len(self), dtstr, self.data.bid[0], self.data.ask[0])) if self.p.sma: txt += ' - SMA: %.4f' % self.sma[0] print(txt) def parse_args(): parser = argparse.ArgumentParser( description='Bid/Ask Line Hierarchy', formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument('--data', '-d', action='store', required=False, default='../../datas/bidask.csv', help='data to add to the system') parser.add_argument('--dtformat', '-dt', required=False, default='%m/%d/%Y %H:%M:%S', help='Format of datetime in input') parser.add_argument('--sma', '-s', action='store_true', required=False, help='Add an SMA to the mix') parser.add_argument('--period', '-p', action='store', required=False, default=5, type=int, help='Period for the sma') return parser.parse_args() def runstrategy(): args = parse_args() cerebro = bt.Cerebro() # Create a cerebro data = BidAskCSV(dataname=args.data, dtformat=args.dtformat) cerebro.adddata(data) # Add the 1st data to cerebro # Add the strategy to cerebro cerebro.addstrategy(St, sma=args.sma, period=args.period) cerebro.run() if __name__ == '__main__': runstrategy()
发布版本 1.2.1.88
原文:
www.backtrader.com/blog/posts/2016-03-07-release-1.2.1.88/release-1.2.1.88/
将次版本号从 1 更改为 2 花费了一些时间,但旧的 DataResampler 和 DataReplayer 的弃用导致了这一变化。
readthedocs上的文档
文档已更新,只引用了现代化的resampling
和replaying
方式。操作如下:
... data = backtrader.feeds.BacktraderCSVData(dataname='mydata.csv') # daily bars cerebro.resampledata(data, timeframe=backtrader.TimeFrame.Weeks) # to weeks ...
对于replaying只需将resampledata
更改为replaydata
。还有其他方法可以做到,但这是最直接的接口,可能是唯一会被任何人使用的接口。
根据Ticket #60,很明显扩展机制允许向数据源(实际上是任何基于lines的对象)添加额外行不足以支持票号中建议的内容。
因此,实现了一个额外的parameter到lines对象,允许完全重新定义行层次结构(逃离 OHLC 领域可能是一个合适的电影标题)
已经添加了一个名为data-bid-ask的示例到数据源中。从这个示例中:
class BidAskCSV(btfeeds.GenericCSVData): linesoverride = True # discard usual OHLC structure # datetime must be present and last lines = ('bid', 'ask', 'datetime') # datetime (always 1st) and then the desired order for params = ( ('dtformat', '%m/%d/%Y %H:%M:%S'), ('datetime', 0), # field pos 0 ('bid', 1), # default field pos 1 ('ask', 2), # defult field pos 2 )
通过指定linesoverride
,常规的lines继承机制被绕过,对象中定义的行将取代任何先前的行。
此版本可从pypi获取,并可通过常规方式安装:
pip install backtrader
或者如果更新:
pip install backtrader --upgrade
2015
数据过滤器
原文:
www.backtrader.com/blog/posts/2015-11-21-data-filters/data-filling-filtering/
一段时间前,票证#23 让我考虑在该票证的上下文中进行的讨论的潜在改进。
在票证中,我添加了一个DataFilter
类,但这太过复杂。实际上,这让人想起了内置了相同功能的DataResampler
和DataReplayer
中构建的复杂性。
因此,自几个版本以来,backtrader
支持向数据源添加一个filter
(如果愿意,可以称之为processor
)。重新采样和重播使用该功能进行了内部重新实现,一切似乎变得不那么复杂(尽管仍然是)
过滤器在起作用
鉴于现有的数据源,您可以使用数据源的addfilter
方法:
data = MyDataFeed(name=myname) data.addfilter(filter, *args, **kwargs)
显然,filter
必须符合给定的接口,即:
- 一个接受此签名的可调用对象:
callable(data, *args, **kwargs)`
或者
- 一个可以实例化和调用的类
- 在实例化期间,init方法必须支持以下签名:
def __init__(self, data, *args, **kwargs)`
- 这个对象的call和 last 方法如下:
def __call__(self, data) def last(self, data)`
可调用/实例将被调用以处理数据源产生的每个数据。
对票证#23 的更好解决方案
那张票想要:
- 一个基于白天的相对成交量指标
- 白天数据可能丢失
- 预/后市场数据可能到达
实施几个过滤器可以缓解回测环境的情况。
过滤预/后市场数据
以下过滤器(已经在backtrader
中可用)挺身而出:
class SessionFilter(with_metaclass(metabase.MetaParams, object)): ''' This class can be applied to a data source as a filter and will filter out intraday bars which fall outside of the regular session times (ie: pre/post market data) This is a "non-simple" filter and must manage the stack of the data (passed during init and __call__) It needs no "last" method because it has nothing to deliver ''' def __init__(self, data): pass def __call__(self, data): ''' Return Values: - False: data stream was not touched - True: data stream was manipulated (bar outside of session times and - removed) ''' if data.sessionstart <= data.datetime.tm(0) <= data.sessionend: # Both ends of the comparison are in the session return False # say the stream is untouched # bar outside of the regular session times data.backwards() # remove bar from data stack return True # signal the data was manipulated
该过滤器使用数据中嵌入的会话开始/结束时间来过滤条形图
- 如果新数据的日期时间在会话时间内,则返回
False
以指示数据未受影响 - 如果日期时间超出范围,则数据源将向后发送,有效地擦除最后生成的数据。并返回
True
以指示数据流已被操作。
注意
调用data.backwards()
可能/可能是低级的,过滤器应该具有处理数据流内部的 API
脚本末尾的示例代码可以在有或无过滤的情况下运行。第一次运行是 100%未经过滤且未指定会话时间:
$ ./data-filler.py --writer --wrcsv
查看第 1 天的开始和结束:
=============================================================================== Id,2006-01-02-volume-min-001,len,datetime,open,high,low,close,volume,openinterest,Strategy,len 1,2006-01-02-volume-min-001,1,2006-01-02 09:01:00,3602.0,3603.0,3597.0,3599.0,5699.0,0.0,Strategy,1 2,2006-01-02-volume-min-001,2,2006-01-02 09:02:00,3600.0,3601.0,3598.0,3599.0,894.0,0.0,Strategy,2 ... ... 581,2006-01-02-volume-min-001,581,2006-01-02 19:59:00,3619.0,3619.0,3619.0,3619.0,1.0,0.0,Strategy,581 582,2006-01-02-volume-min-001,582,2006-01-02 20:00:00,3618.0,3618.0,3617.0,3618.0,242.0,0.0,Strategy,582 583,2006-01-02-volume-min-001,583,2006-01-02 20:01:00,3618.0,3618.0,3617.0,3617.0,15.0,0.0,Strategy,583 584,2006-01-02-volume-min-001,584,2006-01-02 20:04:00,3617.0,3617.0,3617.0,3617.0,107.0,0.0,Strategy,584 585,2006-01-02-volume-min-001,585,2006-01-03 09:01:00,3623.0,3625.0,3622.0,3624.0,4026.0,0.0,Strategy,585 ...
2006 年 1 月 2 日从 09:01:00 到 20:04:00 进行会话运行。
现在使用SessionFilter
运行,并告诉脚本使用 09:30 和 17:30 作为会话的开始/结束时间:
$ ./data-filler.py --writer --wrcsv --tstart 09:30 --tend 17:30 --filter =============================================================================== Id,2006-01-02-volume-min-001,len,datetime,open,high,low,close,volume,openinterest,Strategy,len 1,2006-01-02-volume-min-001,1,2006-01-02 09:30:00,3604.0,3605.0,3603.0,3604.0,546.0,0.0,Strategy,1 2,2006-01-02-volume-min-001,2,2006-01-02 09:31:00,3604.0,3606.0,3604.0,3606.0,438.0,0.0,Strategy,2 ... ... 445,2006-01-02-volume-min-001,445,2006-01-02 17:29:00,3621.0,3621.0,3620.0,3620.0,866.0,0.0,Strategy,445 446,2006-01-02-volume-min-001,446,2006-01-02 17:30:00,3620.0,3621.0,3619.0,3621.0,1670.0,0.0,Strategy,446 447,2006-01-02-volume-min-001,447,2006-01-03 09:30:00,3637.0,3638.0,3635.0,3636.0,1458.0,0.0,Strategy,447 ...
数据输出现在从 09:30 开始,到 17:30 结束。已经过滤掉预/后市场数据。
BackTrader 中文文档(二十五)(2)https://developer.aliyun.com/article/1505458