BackTrader 中文文档(二十二)(1)https://developer.aliyun.com/article/1505425
样本的使用
$ ./macdsystem.py --help usage: macdsystem.py [-h] (--data DATA | --dataset {yhoo,orcl,nvda}) [--fromdate FROMDATE] [--todate TODATE] [--cash CASH] [--cashalloc CASHALLOC] [--commperc COMMPERC] [--macd1 MACD1] [--macd2 MACD2] [--macdsig MACDSIG] [--atrperiod ATRPERIOD] [--atrdist ATRDIST] [--smaperiod SMAPERIOD] [--dirperiod DIRPERIOD] [--riskfreerate RISKFREERATE] [--plot [kwargs]] Sample for Tharp example with MACD optional arguments: -h, --help show this help message and exit --data DATA Specific data to be read in (default: None) --dataset {yhoo,orcl,nvda} Choose one of the predefined data sets (default: None) --fromdate FROMDATE Starting date in YYYY-MM-DD format (default: 2005-01-01) --todate TODATE Ending date in YYYY-MM-DD format (default: None) --cash CASH Cash to start with (default: 50000) --cashalloc CASHALLOC Perc (abs) of cash to allocate for ops (default: 0.2) --commperc COMMPERC Perc (abs) commision in each operation. 0.001 -> 0.1%, 0.01 -> 1% (default: 0.0033) --macd1 MACD1 MACD Period 1 value (default: 12) --macd2 MACD2 MACD Period 2 value (default: 26) --macdsig MACDSIG MACD Signal Period value (default: 9) --atrperiod ATRPERIOD ATR Period To Consider (default: 14) --atrdist ATRDIST ATR Factor for stop price calculation (default: 3.0) --smaperiod SMAPERIOD Period for the moving average (default: 30) --dirperiod DIRPERIOD Period for SMA direction calculation (default: 10) --riskfreerate RISKFREERATE Risk free rate in Perc (abs) of the asset for the Sharpe Ratio (default: 0.01) --plot [kwargs], -p [kwargs] Plot the read data applying any kwargs passed For example: --plot style="candle" (to plot candles) (default: None)
还有代码本身
from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import random import backtrader as bt BTVERSION = tuple(int(x) for x in bt.__version__.split('.')) class FixedPerc(bt.Sizer): '''This sizer simply returns a fixed size for any operation Params: - ``perc`` (default: ``0.20``) Perc of cash to allocate for operation ''' params = ( ('perc', 0.20), # perc of cash to use for operation ) def _getsizing(self, comminfo, cash, data, isbuy): cashtouse = self.p.perc * cash if BTVERSION > (1, 7, 1, 93): size = comminfo.getsize(data.close[0], cashtouse) else: size = cashtouse // data.close[0] return size class TheStrategy(bt.Strategy): ''' This strategy is loosely based on some of the examples from the Van K. Tharp book: *Trade Your Way To Financial Freedom*. The logic: - Enter the market if: - The MACD.macd line crosses the MACD.signal line to the upside - The Simple Moving Average has a negative direction in the last x periods (actual value below value x periods ago) - Set a stop price x times the ATR value away from the close - If in the market: - Check if the current close has gone below the stop price. If yes, exit. - If not, update the stop price if the new stop price would be higher than the current ''' params = ( # Standard MACD Parameters ('macd1', 12), ('macd2', 26), ('macdsig', 9), ('atrperiod', 14), # ATR Period (standard) ('atrdist', 3.0), # ATR distance for stop price ('smaperiod', 30), # SMA Period (pretty standard) ('dirperiod', 10), # Lookback period to consider SMA trend direction ) def notify_order(self, order): if order.status == order.Completed: pass if not order.alive(): self.order = None # indicate no order is pending def __init__(self): self.macd = bt.indicators.MACD(self.data, period_me1=self.p.macd1, period_me2=self.p.macd2, period_signal=self.p.macdsig) # Cross of macd.macd and macd.signal self.mcross = bt.indicators.CrossOver(self.macd.macd, self.macd.signal) # To set the stop price self.atr = bt.indicators.ATR(self.data, period=self.p.atrperiod) # Control market trend self.sma = bt.indicators.SMA(self.data, period=self.p.smaperiod) self.smadir = self.sma - self.sma(-self.p.dirperiod) def start(self): self.order = None # sentinel to avoid operrations on pending order def next(self): if self.order: return # pending order execution if not self.position: # not in the market if self.mcross[0] > 0.0 and self.smadir < 0.0: self.order = self.buy() pdist = self.atr[0] * self.p.atrdist self.pstop = self.data.close[0] - pdist else: # in the market pclose = self.data.close[0] pstop = self.pstop if pclose < pstop: self.close() # stop met - get out else: pdist = self.atr[0] * self.p.atrdist # Update only if greater than self.pstop = max(pstop, pclose - pdist) DATASETS = { 'yhoo': '../../datas/yhoo-1996-2014.txt', 'orcl': '../../datas/orcl-1995-2014.txt', 'nvda': '../../datas/nvda-1999-2014.txt', } def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() cerebro.broker.set_cash(args.cash) comminfo = bt.commissions.CommInfo_Stocks_Perc(commission=args.commperc, percabs=True) cerebro.broker.addcommissioninfo(comminfo) dkwargs = dict() if args.fromdate is not None: fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') dkwargs['fromdate'] = fromdate if args.todate is not None: todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') dkwargs['todate'] = todate # if dataset is None, args.data has been given dataname = DATASETS.get(args.dataset, args.data) data0 = bt.feeds.YahooFinanceCSVData(dataname=dataname, **dkwargs) cerebro.adddata(data0) cerebro.addstrategy(TheStrategy, macd1=args.macd1, macd2=args.macd2, macdsig=args.macdsig, atrperiod=args.atrperiod, atrdist=args.atrdist, smaperiod=args.smaperiod, dirperiod=args.dirperiod) cerebro.addsizer(FixedPerc, perc=args.cashalloc) # Add TimeReturn Analyzers for self and the benchmark data cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='alltime_roi', timeframe=bt.TimeFrame.NoTimeFrame) cerebro.addanalyzer(bt.analyzers.TimeReturn, data=data0, _name='benchmark', timeframe=bt.TimeFrame.NoTimeFrame) # Add TimeReturn Analyzers fot the annuyl returns cerebro.addanalyzer(bt.analyzers.TimeReturn, timeframe=bt.TimeFrame.Years) # Add a SharpeRatio cerebro.addanalyzer(bt.analyzers.SharpeRatio, timeframe=bt.TimeFrame.Years, riskfreerate=args.riskfreerate) # Add SQN to qualify the trades cerebro.addanalyzer(bt.analyzers.SQN) cerebro.addobserver(bt.observers.DrawDown) # visualize the drawdown evol results = cerebro.run() st0 = results[0] for alyzer in st0.analyzers: alyzer.print() if args.plot: pkwargs = dict(style='bar') if args.plot is not True: # evals to True but is not True npkwargs = eval('dict(' + args.plot + ')') # args were passed pkwargs.update(npkwargs) cerebro.plot(**pkwargs) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='Sample for Tharp example with MACD') group1 = parser.add_mutually_exclusive_group(required=True) group1.add_argument('--data', required=False, default=None, help='Specific data to be read in') group1.add_argument('--dataset', required=False, action='store', default=None, choices=DATASETS.keys(), help='Choose one of the predefined data sets') parser.add_argument('--fromdate', required=False, default='2005-01-01', help='Starting date in YYYY-MM-DD format') parser.add_argument('--todate', required=False, default=None, help='Ending date in YYYY-MM-DD format') parser.add_argument('--cash', required=False, action='store', type=float, default=50000, help=('Cash to start with')) parser.add_argument('--cashalloc', required=False, action='store', type=float, default=0.20, help=('Perc (abs) of cash to allocate for ops')) parser.add_argument('--commperc', required=False, action='store', type=float, default=0.0033, help=('Perc (abs) commision in each operation. ' '0.001 -> 0.1%%, 0.01 -> 1%%')) parser.add_argument('--macd1', required=False, action='store', type=int, default=12, help=('MACD Period 1 value')) parser.add_argument('--macd2', required=False, action='store', type=int, default=26, help=('MACD Period 2 value')) parser.add_argument('--macdsig', required=False, action='store', type=int, default=9, help=('MACD Signal Period value')) parser.add_argument('--atrperiod', required=False, action='store', type=int, default=14, help=('ATR Period To Consider')) parser.add_argument('--atrdist', required=False, action='store', type=float, default=3.0, help=('ATR Factor for stop price calculation')) parser.add_argument('--smaperiod', required=False, action='store', type=int, default=30, help=('Period for the moving average')) parser.add_argument('--dirperiod', required=False, action='store', type=int, default=10, help=('Period for SMA direction calculation')) parser.add_argument('--riskfreerate', required=False, action='store', type=float, default=0.01, help=('Risk free rate in Perc (abs) of the asset for ' 'the Sharpe Ratio')) # Plot options parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' '\n' 'For example:\n' '\n' ' --plot style="candle" (to plot candles)\n')) if pargs is not None: return parser.parse_args(pargs) return parser.parse_args() if __name__ == '__main__': runstrat()
Pinkfish 挑战
原文:
www.backtrader.com/blog/posts/2016-07-29-pinkfish-challenge/pinkfish-challenge/
(样本和更改添加到版本 1.7.1.93)
在发展过程中,backtrader 已经变得更加成熟,新增了功能,当然也变得更加复杂。许多新功能是在用户的请求、评论和问题之后引入的。一些小挑战证明了大多数设计决策至少不是那么错误,即使有些事情可能有很多其他方式来完成,有时可能更好。
因此,似乎这些小挑战是为了测试平台对新的未计划和意外情况的灵活性和适应性,pinkfish挑战是另一个例子。pinkfish是另一个 Python 回测框架(在README
中列出),可以在以下网址找到:pinkfish。该网站包含了需要解决的挑战:
- ‘买入收盘价’在‘新的 20 日高点设定’的当天是不允许的
其中一个特点提供了平台如何为这样的壮举运作的提示:
- 使用每日数据(而不是分钟或 tick 数据)进行日内交易
作者对当时现有的回测库的复杂性感到厌烦。当时的情况是否适用于backtrader(当时还处于初期阶段)是由pinkfish的作者自己回答的问题。
无修改解决方案
backtrader 支持数据源的过滤器,其中一个允许
breaking a *daily bar* in 2 parts to let people buy after having seen only the opening price. The 2nd part of the day (high, low, close) is evaluated in a 2nd tick. This effectively achieves the *uses daily data (vs minute or tick data) for intraday trading*.
这个筛选器试图进行完整的重播操作,而不涉及内置的重播器。
这个筛选器的明显演变将每日柱破解为两根柱,第一根是(开盘价,最高价,最低价),然后是第二根完整的柱(开盘价,最高价,最低价,收盘价)。
买入收盘价是通过使用backtrader.Order.Close
作为执行类型来实现的。
这在可用的样本中使用-no-replay
。一个执行:
$ ./pinkfish-challenge.py --no-replay
输出的一部分:
... 0955,0478,0478,2006-11-22T00:00:00,27.51,28.56,27.29,28.49,16027900.00,0.00 High 28.56 > Highest 28.56 LAST 19 highs: array('d', [25.33, 25.6, 26.4, 26.7, 26.62, 26.6, 26.7, 26.7, 27.15, 27.25, 27.65, 27.5, 27.62, 27.5, 27.5, 27.33, 27.05, 27.04, 27.34]) -- BUY on date: 2006-11-22 -- BUY Completed on: 2006-11-22 -- BUY Price: 28.49 0956,0478,0478,2006-11-22T23:59:59.999989,27.51,28.56,27.29,28.49,32055800.00,0.00 ...
它有效…
- 在看到当天第一部分(行:
0955
)之后 - 如果有一个新的 20 日高点,就会发出一个
Close
订单 - 订单是通过当天第二部分的收盘价格执行的(行:
0956
)
收盘价为28.49
,这是在策略中notify_order
中看到的买入价格。
输出包含相当冗长的部分,仅用于识别最后的20
个高点。样本也非常快速出售,以便多次测试行为。但持有期可以通过--sellafter N
进行更改,其中N
是取消之前持有的柱数(请参见末尾的用法)
no mod
解决方案的问题
这实际上不是一个重播解决方案,如果将订单的执行类型从Close
更改为Market
,就会看到这一点。一个新的执行:
$ ./pinkfish-challenge.py --no-replay --market
现在与上述相同期间的输出:
... 0955,0478,0478,2006-11-22T00:00:00,27.51,28.56,27.29,28.49,16027900.00,0.00 High 28.56 > Highest 28.56 LAST 19 highs: array('d', [25.33, 25.6, 26.4, 26.7, 26.62, 26.6, 26.7, 26.7, 27.15, 27.25, 27.65, 27.5, 27.62, 27.5, 27.5, 27.33, 27.05, 27.04, 27.34]) -- BUY on date: 2006-11-22 -- BUY Completed on: 2006-11-22 -- BUY Price: 27.51 0956,0478,0478,2006-11-22T23:59:59.999989,27.51,28.56,27.29,28.49,32055800.00,0.00 ...
问题很容易被识别出来
- 订单执行时,与收盘价相反,因为市价订单取第二根柱中可用的第一价,即
27.51
,而这恰好是当天的开盘价,不再可用。
这是因为过滤器实际上并非真正回放,而是将柱子分成两部分,并进行软性回放。
正确的“mod”解决方案
同样获取Market
订单以选择收盘价。
这包括:
- 一个将柱子分成两部分的过滤器
- 并且与backtrader中可用的标准回放功能兼容
在这种情况下,第二根柱仅由close
价格组成,即使显示显示完整的柱,内部机制也只会将订单与tick匹配
backtrader中的链接过滤器已经是可能的,但这种用法尚未考虑:
- 将单个数据“心跳”拆分为 2 个数据“心跳”
在此挑战之前,主要是将柱子合并为较大的柱子。
对核心机制加载柱进行了小扩展,允许过滤器将柱的第二部分添加到内部存储中以进行重新处理,然后再考虑新数据心跳。而且因为它是一个扩展而不是修改,所以没有影响。
此挑战还提供了机会:
- 再次查看backtrader最初编写的早期代码以获取
Close
订单。
在这里,一些代码行和if
条件已经重新设计,以使匹配Close
订单更加合乎逻辑,并且如果可能的话,将其立即交付给系统(即使匹配到正确的柱上,交付也通常会延迟 1 根柱)
在这些变化之后的一个好处是:
- 过滤器中的逻辑更加简单,因为没有微妙的回放尝试。 回放由回放过滤器完成。
分解柱子第一部分的过滤器解剖:
- 复制传入数据柱
- 将其复制为OHL柱(无 Close)
- 将时间更改为日期 + sessionstart时间
- 移除部分体积(由参数closevol指定给过滤器)
- 使
OpenInterest
失效(在当天结束时可用) - 移除
close
价格并用OHL的平均值替换它 - 将柱子添加到内部栈以供下一个过滤器或策略立即处理(回放过滤器将接管)
分解柱子第二部分的解剖:
- 复制传入数据柱
- 将 OHL 价格替换为
Close
价格 - 将时间更改为日期 + sessionend时间
- 移除体积的其他部分(由参数closevol指定给过滤器)
- 设置
OpenInterest
- 将柱子添加到内部stash以延迟处理为下一个数据心跳,而不是从数据中获取价格
代码:
# Make a copy of current data for ohlbar ohlbar = [data.lines[i][0] for i in range(data.size())] closebar = ohlbar[:] # Make a copy for the close # replace close price with o-h-l average ohlprice = ohlbar[data.Open] + ohlbar[data.High] + ohlbar[data.Low] ohlbar[data.Close] = ohlprice / 3.0 vol = ohlbar[data.Volume] # adjust volume ohlbar[data.Volume] = vohl = int(vol * (1.0 - self.p.closevol)) oi = ohlbar[data.OpenInterest] # adjust open interst ohlbar[data.OpenInterest] = 0 # Adjust times dt = datetime.datetime.combine(datadt, data.p.sessionstart) ohlbar[data.DateTime] = data.date2num(dt) # Adjust closebar to generate a single tick -> close price closebar[data.Open] = cprice = closebar[data.Close] closebar[data.High] = cprice closebar[data.Low] = cprice closebar[data.Volume] = vol - vohl ohlbar[data.OpenInterest] = oi # Adjust times dt = datetime.datetime.combine(datadt, data.p.sessionend) closebar[data.DateTime] = data.date2num(dt) # Update stream data.backwards(force=True) # remove the copied bar from stream data._add2stack(ohlbar) # add ohlbar to stack # Add 2nd part to stash to delay processing to next round data._add2stack(closebar, stash=True) return False # the length of the stream was not changed
在不禁用回放和Close
的情况下执行(让我们添加绘图):
$ ./pinkfish-challenge.py --plot
同一时期的输出:
... 0955,0478,0478,2006-11-22T00:00:00,27.51,28.56,27.29,27.79,16027900.00,0.00 High 28.56 > Highest 28.56 LAST 19 highs: array('d', [25.33, 25.6, 26.4, 26.7, 26.62, 26.6, 26.7, 26.7, 27.15, 27.25, 27.65, 27.5, 27.62, 27.5, 27.5, 27.33, 27.05, 27.04, 27.34]) -- BUY on date: 2006-11-22 -- BUY Completed on: 2006-11-22 -- BUY Price: 28.49 0956,0478,0478,2006-11-22T23:59:59.999989,27.51,28.56,27.29,28.49,32055800.00,0.00 ...
一切正常,已记录收盘价为28.49
。
以及图表。
最后但同样重要的是检查修改是否有意义:
$ ./pinkfish-challenge.py --market
相同时期的输出:
... 0955,0478,0478,2006-11-22T00:00:00,27.51,28.56,27.29,27.79,16027900.00,0.00 High 28.56 > Highest 28.56 LAST 19 highs: array('d', [25.33, 25.6, 26.4, 26.7, 26.62, 26.6, 26.7, 26.7, 27.15, 27.25, 27.65, 27.5, 27.62, 27.5, 27.5, 27.33, 27.05, 27.04, 27.34]) -- BUY on date: 2006-11-22 -- BUY Completed on: 2006-11-22 -- BUY Price: 28.49 0956,0478,0478,2006-11-22T23:59:59.999989,27.51,28.56,27.29,28.49,32055800.00,0.00 ..
现在Market
订单正在以与Close
订单相同的价格28.49
拾取,这在这个特定的用例中是预期的,因为重播正在发生,而破碎的日线的第二部分有一个单一的标记:28.49
,这是收盘价
示例的用法
$ ./pinkfish-challenge.py --help usage: pinkfish-challenge.py [-h] [--data DATA] [--fromdate FROMDATE] [--todate TODATE] [--cash CASH] [--sellafter SELLAFTER] [--highperiod HIGHPERIOD] [--no-replay] [--market] [--oldbuysell] [--plot [kwargs]] Sample for pinkfish challenge optional arguments: -h, --help show this help message and exit --data DATA Data to be read in (default: ../../datas/yhoo-1996-2015.txt) --fromdate FROMDATE Starting date in YYYY-MM-DD format (default: 2005-01-01) --todate TODATE Ending date in YYYY-MM-DD format (default: 2006-12-31) --cash CASH Cash to start with (default: 50000) --sellafter SELLAFTER Sell after so many bars in market (default: 2) --highperiod HIGHPERIOD Period to look for the highest (default: 20) --no-replay Use Replay + replay filter (default: False) --market Use Market exec instead of Close (default: False) --oldbuysell Old buysell plot behavior - ON THE PRICE (default: False) --plot [kwargs], -p [kwargs] Plot the read data applying any kwargs passed For example (escape the quotes if needed): --plot style="candle" (to plot candles) (default: None)
BackTrader 中文文档(二十二)(3)https://developer.aliyun.com/article/1505429