From 1789f935bb4287879aa2f75d70a6b8706038a667 Mon Sep 17 00:00:00 2001 From: liu Date: Fri, 26 Jun 2026 10:57:51 +0800 Subject: [PATCH 1/2] Translate core comments to Chinese --- backtrader/__init__.py | 2 +- backtrader/analyzer.py | 139 ++- backtrader/analyzers/__init__.py | 4 +- backtrader/analyzers/annualreturn.py | 36 +- backtrader/analyzers/calmar.py | 60 +- backtrader/analyzers/drawdown.py | 159 ++- backtrader/analyzers/leverage.py | 33 +- backtrader/analyzers/logreturnsrolling.py | 108 +- backtrader/analyzers/periodstats.py | 57 +- backtrader/analyzers/positions.py | 55 +- backtrader/analyzers/pyfolio.py | 82 +- backtrader/analyzers/returns.py | 97 +- backtrader/analyzers/sharpe.py | 136 +-- backtrader/analyzers/sqn.py | 31 +- backtrader/analyzers/timereturn.py | 114 +- backtrader/analyzers/tradeanalyzer.py | 41 +- backtrader/analyzers/transactions.py | 51 +- backtrader/analyzers/vwr.py | 113 +- backtrader/broker.py | 95 +- backtrader/brokers/__init__.py | 9 +- backtrader/brokers/bbroker.py | 494 ++++---- backtrader/brokers/ibbroker.py | 207 ++-- backtrader/brokers/oandabroker.py | 90 +- backtrader/brokers/vcbroker.py | 145 +-- backtrader/btrun/btrun.py | 123 +- backtrader/cerebro.py | 1017 ++++++++--------- backtrader/comminfo.py | 326 +++--- backtrader/commissions/__init__.py | 9 +- backtrader/dataseries.py | 105 +- backtrader/errors.py | 26 +- backtrader/feed.py | 271 ++--- backtrader/feeds/__init__.py | 6 +- backtrader/feeds/blaze.py | 46 +- backtrader/feeds/btcsv.py | 18 +- backtrader/feeds/chainer.py | 39 +- backtrader/feeds/csvgeneric.py | 92 +- backtrader/feeds/ibdata.py | 424 +++---- backtrader/feeds/influxfeed.py | 33 +- backtrader/feeds/mt4csv.py | 17 +- backtrader/feeds/oanda.py | 223 ++-- backtrader/feeds/pandafeed.py | 177 +-- backtrader/feeds/quandl.py | 131 +-- backtrader/feeds/rollover.py | 109 +- backtrader/feeds/sierrachart.py | 15 +- backtrader/feeds/vcdata.py | 319 +++--- backtrader/feeds/vchart.py | 41 +- backtrader/feeds/vchartcsv.py | 23 +- backtrader/feeds/vchartfile.py | 67 +- backtrader/feeds/yahoo.py | 183 ++- backtrader/fillers.py | 155 ++- backtrader/filters/bsplitter.py | 70 +- backtrader/filters/calendardays.py | 77 +- backtrader/filters/datafiller.py | 80 +- backtrader/filters/datafilter.py | 38 +- backtrader/filters/daysteps.py | 66 +- backtrader/filters/heikinashi.py | 18 +- backtrader/filters/renko.py | 65 +- backtrader/filters/session.py | 212 ++-- backtrader/flt.py | 10 + backtrader/functions.py | 53 +- backtrader/indicator.py | 31 +- backtrader/indicators/__init__.py | 13 +- backtrader/indicators/accdecoscillator.py | 21 +- backtrader/indicators/aroon.py | 137 ++- backtrader/indicators/atr.py | 85 +- backtrader/indicators/awesomeoscillator.py | 18 +- backtrader/indicators/basicops.py | 366 ++++-- backtrader/indicators/bollinger.py | 39 +- backtrader/indicators/cci.py | 22 +- backtrader/indicators/contrib/vortex.py | 19 +- backtrader/indicators/crossover.py | 87 +- backtrader/indicators/dema.py | 44 +- backtrader/indicators/deviation.py | 48 +- backtrader/indicators/directionalmove.py | 271 +++-- backtrader/indicators/dma.py | 38 +- backtrader/indicators/dpo.py | 36 +- backtrader/indicators/dv2.py | 19 +- backtrader/indicators/ema.py | 20 +- backtrader/indicators/envelope.py | 36 +- backtrader/indicators/hadelta.py | 26 +- backtrader/indicators/heikinashi.py | 18 +- backtrader/indicators/hma.py | 37 +- backtrader/indicators/hurst.py | 53 +- backtrader/indicators/ichimoku.py | 29 +- backtrader/indicators/kama.py | 43 +- backtrader/indicators/kst.py | 34 +- backtrader/indicators/lrsi.py | 54 +- backtrader/indicators/mabase.py | 20 +- backtrader/indicators/macd.py | 44 +- backtrader/indicators/momentum.py | 74 +- backtrader/indicators/ols.py | 83 +- backtrader/indicators/oscillator.py | 46 +- backtrader/indicators/percentchange.py | 20 +- backtrader/indicators/percentrank.py | 17 +- backtrader/indicators/pivotpoint.py | 142 ++- backtrader/indicators/prettygoodoscillator.py | 29 +- backtrader/indicators/priceoscillator.py | 74 +- backtrader/indicators/psar.py | 107 +- backtrader/indicators/rmi.py | 35 +- backtrader/indicators/rsi.py | 177 ++- backtrader/indicators/sma.py | 18 +- backtrader/indicators/smma.py | 24 +- backtrader/indicators/stochastic.py | 74 +- backtrader/indicators/trix.py | 44 +- backtrader/indicators/tsi.py | 27 +- backtrader/indicators/ultimateoscillator.py | 26 +- backtrader/indicators/vortex.py | 14 + backtrader/indicators/williams.py | 44 +- backtrader/indicators/wma.py | 19 +- backtrader/indicators/zlema.py | 19 +- backtrader/indicators/zlind.py | 40 +- backtrader/linebuffer.py | 465 ++++---- backtrader/lineiterator.py | 144 +-- backtrader/lineroot.py | 216 +++- backtrader/lineseries.py | 294 +++-- backtrader/mathsupport.py | 46 +- backtrader/metabase.py | 77 +- backtrader/observer.py | 14 +- backtrader/observers/__init__.py | 4 +- backtrader/observers/benchmark.py | 88 +- backtrader/observers/broker.py | 74 +- backtrader/observers/buysell.py | 46 +- backtrader/observers/drawdown.py | 65 +- backtrader/observers/logreturns.py | 61 +- backtrader/observers/timereturn.py | 49 +- backtrader/observers/trades.py | 45 +- backtrader/order.py | 377 ++++-- backtrader/plot/__init__.py | 5 +- backtrader/plot/finance.py | 84 +- backtrader/plot/formatters.py | 10 +- backtrader/plot/locator.py | 50 +- backtrader/plot/multicursor.py | 63 +- backtrader/plot/plot.py | 159 +-- backtrader/plot/scheme.py | 90 +- backtrader/plot/utils.py | 47 +- backtrader/position.py | 164 ++- backtrader/resamplerfilter.py | 302 +++-- backtrader/signal.py | 2 + backtrader/sizer.py | 73 +- backtrader/sizers/__init__.py | 4 +- backtrader/sizers/fixedsize.py | 93 +- backtrader/sizers/percents_sizer.py | 77 +- backtrader/store.py | 36 +- backtrader/stores/__init__.py | 9 +- backtrader/stores/ibstore.py | 619 +++++----- backtrader/stores/oandastore.py | 141 ++- backtrader/stores/vchartfile.py | 28 +- backtrader/stores/vcstore.py | 184 ++- backtrader/strategies/sma_crossover.py | 37 +- backtrader/strategy.py | 960 ++++++---------- backtrader/studies/contrib/fractal.py | 29 +- backtrader/talib.py | 76 +- backtrader/timer.py | 65 +- backtrader/trade.py | 279 +++-- backtrader/tradingcal.py | 173 ++- backtrader/utils/autodict.py | 45 +- backtrader/utils/dateintern.py | 105 +- backtrader/utils/flushfile.py | 9 + backtrader/utils/ordereddefaultdict.py | 21 +- backtrader/utils/py3.py | 9 +- backtrader/writer.py | 68 +- samples/calendar-days/calendar-days.py | 16 +- tests/fake_data/README.md | 20 + tests/fake_data/scenarios.py | 281 +++++ tests/fake_data/tiny-ohlcv.csv | 7 + 165 files changed, 9115 insertions(+), 7293 deletions(-) create mode 100644 tests/fake_data/README.md create mode 100644 tests/fake_data/scenarios.py create mode 100644 tests/fake_data/tiny-ohlcv.csv diff --git a/backtrader/__init__.py b/backtrader/__init__.py index 15770f55a..8a3651334 100644 --- a/backtrader/__init__.py +++ b/backtrader/__init__.py @@ -85,6 +85,6 @@ from . import talib as talib -# Load contributed indicators and studies +# 加载 contributed indicators 和 studies import backtrader.indicators.contrib import backtrader.studies.contrib diff --git a/backtrader/analyzer.py b/backtrader/analyzer.py index 51abc02a5..47aef1af3 100644 --- a/backtrader/analyzer.py +++ b/backtrader/analyzer.py @@ -34,9 +34,9 @@ class MetaAnalyzer(bt.MetaParams): def donew(cls, *args, **kwargs): ''' - Intercept the strategy parameter + 拦截 strategy 参数。 ''' - # Create the object and set the params in place + # 创建对象并设置 params _obj, args, kwargs = super(MetaAnalyzer, cls).donew(*args, **kwargs) _obj._children = list() @@ -44,14 +44,14 @@ def donew(cls, *args, **kwargs): _obj.strategy = strategy = bt.metabase.findowner(_obj, bt.Strategy) _obj._parent = bt.metabase.findowner(_obj, Analyzer) - # Register with a master observer if created inside one + # 如果在 master observer 内创建,则向其注册 masterobs = bt.metabase.findowner(_obj, bt.Observer) if masterobs is not None: masterobs._register_analyzer(_obj) _obj.datas = strategy.datas - # For each data add aliases: for first data: data and data0 + # 为每个 data 添加 alias:第一个 data 同时是 data 和 data0 if _obj.datas: _obj.data = data = _obj.datas[0] @@ -72,7 +72,7 @@ def donew(cls, *args, **kwargs): _obj.create_analysis() - # Return to the normal chain + # 回到正常调用链 return _obj, args, kwargs def dopostinit(cls, _obj, *args, **kwargs): @@ -82,25 +82,23 @@ def dopostinit(cls, _obj, *args, **kwargs): if _obj._parent is not None: _obj._parent._register(_obj) - # Return to the normal chain + # 回到正常调用链 return _obj, args, kwargs class Analyzer(with_metaclass(MetaAnalyzer, object)): - '''Analyzer base class. All analyzers are subclass of this one + '''Analyzer 的基类,用于在 strategy 运行过程中收集并返回分析结果。 - An Analyzer instance operates in the frame of a strategy and provides an - analysis for that strategy. + Analyzer 实例在 strategy 的上下文中运行,并为该 strategy 提供分析结果。 - Automagically set member attributes: + 自动设置的成员属性: - - ``self.strategy`` (giving access to the *strategy* and anything - accessible from it) + - ``self.strategy``: 访问 *strategy* 以及 strategy 可访问的所有内容 - - ``self.datas[x]`` giving access to the array of data feeds present in - the the system, which could also be accessed via the strategy reference + - ``self.datas[x]``: 访问系统中的 data feeds 数组,也可通过 strategy + 引用访问 - - ``self.data``, giving access to ``self.datas[0]`` + - ``self.data``: 访问 ``self.datas[0]`` - ``self.dataX`` -> ``self.datas[X]`` @@ -112,34 +110,33 @@ class Analyzer(with_metaclass(MetaAnalyzer, object)): - ``self.data_Y`` -> ``self.datas[0].lines[Y]`` - This is not a *Lines* object, but the methods and operation follow the same - design + Analyzer 不是 *Lines* 对象,但方法和运行方式遵循相同设计: - - ``__init__`` during instantiation and initial setup + - ``__init__``: 实例化和初始设置阶段调用 - - ``start`` / ``stop`` to signal the begin and end of operations + - ``start`` / ``stop``: 标记运行开始和结束 - - ``prenext`` / ``nextstart`` / ``next`` family of methods that follow - the calls made to the same methods in the strategy + - ``prenext`` / ``nextstart`` / ``next``: 跟随 strategy 中同名方法的调用 - ``notify_trade`` / ``notify_order`` / ``notify_cashvalue`` / - ``notify_fund`` which receive the same notifications as the equivalent - methods of the strategy + ``notify_fund``: 接收与 strategy 中同名方法相同的通知 - The mode of operation is open and no pattern is preferred. As such the - analysis can be generated with the ``next`` calls, at the end of operations - during ``stop`` and even with a single method like ``notify_trade`` + Analyzer 的运行模式是开放的,不强制某一种模式。因此分析结果既可以在 + ``next`` 调用中生成,也可以在运行结束时的 ``stop`` 中生成,甚至可以只用 + ``notify_trade`` 这类单个方法生成。 - The important thing is to override ``get_analysis`` to return a *dict-like* - object containing the results of the analysis (the actual format is - implementation dependent) + 子类最重要的是覆盖 ``get_analysis``,返回包含分析结果的 *dict-like* + 对象。实际格式由具体实现决定。 ''' csv = True def __len__(self): - '''Support for invoking ``len`` on analyzers by actually returning the - current length of the strategy the analyzer operates on''' + '''支持对 analyzer 调用 ``len``。 + + Returns: + int: analyzer 所属 strategy 的当前长度。 + ''' return len(self.strategy) def _register(self, child): @@ -200,77 +197,92 @@ def _stop(self): self.stop() def notify_cashvalue(self, cash, value): - '''Receives the cash/value notification before each next cycle''' + '''在每个 next cycle 前接收 cash/value 通知。 + + Args: + cash (float): 当前 cash。 + value (float): 当前 portfolio value。 + ''' pass def notify_fund(self, cash, value, fundvalue, shares): - '''Receives the current cash, value, fundvalue and fund shares''' + '''接收当前 cash、value、fundvalue 和 fund shares。 + + Args: + cash (float): 当前 cash。 + value (float): 当前 portfolio value。 + fundvalue (float): 当前 fund value。 + shares (float): 当前 fund shares。 + ''' pass def notify_order(self, order): - '''Receives order notifications before each next cycle''' + '''在每个 next cycle 前接收 order 通知。 + + Args: + order: 发生状态变化的 order。 + ''' pass def notify_trade(self, trade): - '''Receives trade notifications before each next cycle''' + '''在每个 next cycle 前接收 trade 通知。 + + Args: + trade: 发生状态变化的 trade。 + ''' pass def next(self): - '''Invoked for each next invocation of the strategy, once the minum - preiod of the strategy has been reached''' + '''当 strategy 达到最小 period 后,随 strategy 的每次 next 调用而调用。''' pass def prenext(self): - '''Invoked for each prenext invocation of the strategy, until the minimum - period of the strategy has been reached + '''在 strategy 达到最小 period 前,随 strategy 的每次 prenext 调用而调用。 - The default behavior for an analyzer is to invoke ``next`` + 默认行为是调用 ``next``。 ''' self.next() def nextstart(self): - '''Invoked exactly once for the nextstart invocation of the strategy, - when the minimum period has been first reached + '''当 strategy 首次达到最小 period 时,随 strategy 的 nextstart 调用一次。 ''' self.next() def start(self): - '''Invoked to indicate the start of operations, giving the analyzer - time to setup up needed things''' + '''运行开始时调用,用于让 analyzer 设置所需状态。''' pass def stop(self): - '''Invoked to indicate the end of operations, giving the analyzer - time to shut down needed things''' + '''运行结束时调用,用于让 analyzer 收尾或生成最终结果。''' pass def create_analysis(self): - '''Meant to be overriden by subclasses. Gives a chance to create the - structures that hold the analysis. + '''供子类覆盖,用于创建保存分析结果的数据结构。 - The default behaviour is to create a ``OrderedDict`` named ``rets`` + 默认行为是创建名为 ``rets`` 的 ``OrderedDict``。 ''' self.rets = OrderedDict() def get_analysis(self): - '''Returns a *dict-like* object with the results of the analysis - - The keys and format of analysis results in the dictionary is - implementation dependent. + '''返回包含分析结果的 *dict-like* 对象。 - It is not even enforced that the result is a *dict-like object*, just - the convention + Returns: + dict-like: 分析结果。key 和结果格式由具体实现决定。 - The default implementation returns the default OrderedDict ``rets`` - created by the default ``create_analysis`` method + 这里并不强制返回值一定是 *dict-like object*,这只是约定。默认实现返回 + 由默认 ``create_analysis`` 方法创建的 ``OrderedDict`` ``rets``。 ''' return self.rets def print(self, *args, **kwargs): - '''Prints the results returned by ``get_analysis`` via a standard - ``Writerfile`` object, which defaults to writing things to standard - output + '''通过标准 ``WriterFile`` 对象打印 ``get_analysis`` 返回的结果。 + + Args: + *args: 传给 ``WriterFile`` 的位置参数。 + **kwargs: 传给 ``WriterFile`` 的关键字参数。 + + 默认会写到 standard output。 ''' writer = bt.WriterFile(*args, **kwargs) writer.start() @@ -280,8 +292,11 @@ def print(self, *args, **kwargs): writer.stop() def pprint(self, *args, **kwargs): - '''Prints the results returned by ``get_analysis`` using the pretty - print Python module (*pprint*) + '''使用 Python 的 pretty print 模块(*pprint*)打印分析结果。 + + Args: + *args: 传给 ``pprint`` 的位置参数。 + **kwargs: 传给 ``pprint`` 的关键字参数。 ''' pp.pprint(self.get_analysis(), *args, **kwargs) diff --git a/backtrader/analyzers/__init__.py b/backtrader/analyzers/__init__.py index e54cc09ad..0ae0846f7 100644 --- a/backtrader/analyzers/__init__.py +++ b/backtrader/analyzers/__init__.py @@ -21,8 +21,8 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) -# The modules below should/must define __all__ with the objects wishes -# or prepend an "_" (underscore) to private classes/variables +# 下方模块应定义 __all__ 来声明导出对象,或给私有 class/variable 加上 +# "_" 前缀 from .annualreturn import * from .drawdown import * diff --git a/backtrader/analyzers/annualreturn.py b/backtrader/analyzers/annualreturn.py index d29024b52..b1b7d5b21 100644 --- a/backtrader/analyzers/annualreturn.py +++ b/backtrader/analyzers/annualreturn.py @@ -28,27 +28,29 @@ class AnnualReturn(Analyzer): - ''' - This analyzer calculates the AnnualReturns by looking at the beginning - and end of the year - - Params: + '''按自然年计算年度收益率的 analyzer。 - - (None) + 该 analyzer 会比较每一年的起始 value 和结束 value,生成年度 return。 - Member Attributes: + Args: + 无。 - - ``rets``: list of calculated annual returns + Returns: + OrderedDict: ``get_analysis`` 返回以年份为 key、年度 return 为 value + 的字典。 - - ``ret``: dictionary (key: year) of annual returns - - **get_analysis**: + Member Attributes: + rets (list): 已计算的年度 return 列表。 + ret (OrderedDict): 以年份为 key 的年度 return 字典。 - - Returns a dictionary of annual returns (key: year) + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addanalyzer(AnnualReturn, _name='annual') ''' def stop(self): - # Must have stats.broker + # 必须有 stats.broker cur_year = -1 value_start = 0.0 @@ -68,19 +70,19 @@ def stop(self): self.rets.append(annualret) self.ret[cur_year] = annualret - # changing between real years, use last value as new start + # 跨自然年时,使用上一年最后 value 作为新的起点 value_start = value_end else: - # No value set whatsoever, use the currently loaded value + # 尚未设置任何 value,使用当前已加载 value value_start = value_cur cur_year = dt.year - # No matter what, the last value is always the last loaded value + # 无论如何,最后 value 始终是最后加载到的 value value_end = value_cur if cur_year not in self.ret: - # finish calculating pending data + # 完成待处理数据的计算 annualret = (value_end / value_start) - 1.0 self.rets.append(annualret) self.ret[cur_year] = annualret diff --git a/backtrader/analyzers/calmar.py b/backtrader/analyzers/calmar.py index 97cb59c04..9373ac36d 100644 --- a/backtrader/analyzers/calmar.py +++ b/backtrader/analyzers/calmar.py @@ -29,53 +29,41 @@ class Calmar(bt.TimeFrameAnalyzerBase): - '''This analyzer calculates the CalmarRatio - timeframe which can be different from the one used in the underlying data - Params: + '''计算 Calmar Ratio 的 analyzer。 - - ``timeframe`` (default: ``None``) - If ``None`` the ``timeframe`` of the 1st data in the system will be - used + 统计使用的 timeframe 可以不同于底层 data 使用的 timeframe。 - Pass ``TimeFrame.NoTimeFrame`` to consider the entire dataset with no - time constraints + Args: + timeframe: 统计使用的 timeframe,默认 ``TimeFrame.Months``。 + compression: timeframe 压缩倍数,默认 ``None``。仅用于日内 + timeframe。如果为 ``None``,使用系统中第 1 个 data 的 + compression。 + period (int): rolling 计算使用的 period 数量,默认 ``36``。 + fund: 如果为 ``None``,会自动检测 broker 的实际模式(fundmode - + True/False),以决定 returns 基于总净资产 value 还是 fund value。 + 将其设为 ``True`` 或 ``False`` 可指定具体行为。 - - ``compression`` (default: ``None``) + Returns: + OrderedDict: ``get_analysis`` 返回以时间 period 为 key、rolling + Calmar Ratio 为 value 的字典。 - Only used for sub-day timeframes to for example work on an hourly - timeframe by specifying "TimeFrame.Minutes" and 60 as compression - - If ``None`` then the compression of the 1st data of the system will be - used - - *None* - - - ``fund`` (default: ``None``) - - If ``None`` the actual mode of the broker (fundmode - True/False) will - be autodetected to decide if the returns are based on the total net - asset value or on the fund value. See ``set_fundmode`` in the broker - documentation - - Set it to ``True`` or ``False`` for a specific behavior + Attributes: + calmar: 最近一次计算得到的 Calmar Ratio。 See also: + https://en.wikipedia.org/wiki/Calmar_ratio - - https://en.wikipedia.org/wiki/Calmar_ratio - - Methods: - - ``get_analysis`` - - Returns a OrderedDict with a key for the time period and the - corresponding rolling Calmar ratio - - Attributes: - - ``calmar`` the latest calculated calmar ratio + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addanalyzer(Calmar, timeframe=bt.TimeFrame.Months, + ... period=36, _name='calmar') ''' packages = ('collections', 'math',) params = ( - ('timeframe', bt.TimeFrame.Months), # default in calmar + ('timeframe', bt.TimeFrame.Months), # calmar 默认值 ('period', 36), ('fund', None), ) @@ -110,4 +98,4 @@ def on_dt_over(self): self.rets[self.dtkey] = calmar def stop(self): - self.on_dt_over() # update last values + self.on_dt_over() # 更新最后一组 value diff --git a/backtrader/analyzers/drawdown.py b/backtrader/analyzers/drawdown.py index 022168972..7cfdc1454 100644 --- a/backtrader/analyzers/drawdown.py +++ b/backtrader/analyzers/drawdown.py @@ -29,35 +29,31 @@ class DrawDown(bt.Analyzer): - '''This analyzer calculates trading system drawdowns stats such as drawdown - values in %s and in dollars, max drawdown in %s and in dollars, drawdown - length and drawdown max length - - Params: - - - ``fund`` (default: ``None``) - - If ``None`` the actual mode of the broker (fundmode - True/False) will - be autodetected to decide if the returns are based on the total net - asset value or on the fund value. See ``set_fundmode`` in the broker - documentation - - Set it to ``True`` or ``False`` for a specific behavior - - Methods: - - - ``get_analysis`` - - Returns a dictionary (with . notation support and subdctionaries) with - drawdown stats as values, the following keys/attributes are available: - - - ``drawdown`` - drawdown value in 0.xx % - - ``moneydown`` - drawdown value in monetary units - - ``len`` - drawdown length - - - ``max.drawdown`` - max drawdown value in 0.xx % - - ``max.moneydown`` - max drawdown value in monetary units - - ``max.len`` - max drawdown length + '''计算交易系统 drawdown 统计信息的 analyzer。 + + 统计内容包括当前 drawdown、金额回撤、最大 drawdown、最大金额回撤、 + 当前 drawdown 持续长度和最大持续长度。 + + Args: + fund: 如果为 ``None``,会自动检测 broker 的实际模式(fundmode - + True/False),以决定 drawdown 基于总净资产 value 还是 fund + value。将其设为 ``True`` 或 ``False`` 可指定具体行为。 + + Returns: + AutoOrderedDict: ``get_analysis`` 返回支持 ``.`` 访问的 dict-like + 对象,包含以下 key: + + - ``drawdown``: 当前 drawdown,单位为 0.xx % + - ``moneydown``: 当前金额回撤 + - ``len``: 当前 drawdown 持续长度 + - ``max.drawdown``: 最大 drawdown,单位为 0.xx % + - ``max.moneydown``: 最大金额回撤 + - ``max.len``: 最大 drawdown 持续长度 + + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addanalyzer(DrawDown, _name='drawdown') ''' params = ( @@ -72,7 +68,7 @@ def start(self): self._fundmode = self.p.fund def create_analysis(self): - self.rets = AutoOrderedDict() # dict with . notation + self.rets = AutoOrderedDict() # 支持 . notation 的 dict self.rets.len = 0 self.rets.drawdown = 0.0 @@ -82,27 +78,27 @@ def create_analysis(self): self.rets.max.drawdown = 0.0 self.rets.max.moneydown = 0.0 - self._maxvalue = float('-inf') # any value will outdo it + self._maxvalue = float('-inf') # 任意 value 都会高于它 def stop(self): - self.rets._close() # . notation cannot create more keys + self.rets._close() # . notation 不能再创建更多 key def notify_fund(self, cash, value, fundvalue, shares): if not self._fundmode: - self._value = value # record current value - self._maxvalue = max(self._maxvalue, value) # update peak value + self._value = value # 记录当前 value + self._maxvalue = max(self._maxvalue, value) # 更新峰值 value else: - self._value = fundvalue # record current value - self._maxvalue = max(self._maxvalue, fundvalue) # update peak + self._value = fundvalue # 记录当前 value + self._maxvalue = max(self._maxvalue, fundvalue) # 更新峰值 def next(self): r = self.rets - # calculate current drawdown values + # 计算当前 drawdown 值 r.moneydown = moneydown = self._maxvalue - self._value r.drawdown = drawdown = 100.0 * moneydown / self._maxvalue - # maxximum drawdown values + # 最大 drawdown 值 r.max.moneydown = max(r.max.moneydown, moneydown) r.max.drawdown = maxdrawdown = max(r.max.drawdown, drawdown) @@ -111,50 +107,39 @@ def next(self): class TimeDrawDown(bt.TimeFrameAnalyzerBase): - '''This analyzer calculates trading system drawdowns on the chosen - timeframe which can be different from the one used in the underlying data - Params: - - - ``timeframe`` (default: ``None``) - If ``None`` the ``timeframe`` of the 1st data in the system will be - used - - Pass ``TimeFrame.NoTimeFrame`` to consider the entire dataset with no - time constraints - - - ``compression`` (default: ``None``) - - Only used for sub-day timeframes to for example work on an hourly - timeframe by specifying "TimeFrame.Minutes" and 60 as compression - - If ``None`` then the compression of the 1st data of the system will be - used - - *None* - - - ``fund`` (default: ``None``) - - If ``None`` the actual mode of the broker (fundmode - True/False) will - be autodetected to decide if the returns are based on the total net - asset value or on the fund value. See ``set_fundmode`` in the broker - documentation - - Set it to ``True`` or ``False`` for a specific behavior - - Methods: - - - ``get_analysis`` - - Returns a dictionary (with . notation support and subdctionaries) with - drawdown stats as values, the following keys/attributes are available: - - - ``drawdown`` - drawdown value in 0.xx % - - ``maxdrawdown`` - drawdown value in monetary units - - ``maxdrawdownperiod`` - drawdown length - - - Those are available during runs as attributes - - ``dd`` - - ``maxdd`` - - ``maxddlen`` + '''按指定 timeframe 计算交易系统 drawdown 的 analyzer。 + + 该 timeframe 可以不同于底层 data 使用的 timeframe。 + + Args: + timeframe: 统计使用的 timeframe,默认 ``None``。如果为 ``None``, + 使用系统中第 1 个 data 的 timeframe。传入 + ``TimeFrame.NoTimeFrame`` 可在不受时间约束的情况下考虑整个 + dataset。 + compression: timeframe 压缩倍数,默认 ``None``。仅用于日内 + timeframe。如果为 ``None``,使用系统中第 1 个 data 的 + compression。 + fund: 如果为 ``None``,会自动检测 broker 的实际模式(fundmode - + True/False),以决定 drawdown 基于总净资产 value 还是 fund + value。将其设为 ``True`` 或 ``False`` 可指定具体行为。 + + Returns: + dict: ``get_analysis`` 返回包含以下 key 的字典: + + - ``maxdrawdown``: 最大 drawdown + - ``maxdrawdownperiod``: 最大 drawdown 持续 period + + 运行过程中也可以直接读取以下属性: + + - ``dd`` + - ``maxdd`` + - ``maxddlen`` + + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addanalyzer(TimeDrawDown, timeframe=bt.TimeFrame.Months, + ... _name='timedrawdown') ''' params = ( @@ -179,16 +164,16 @@ def on_dt_over(self): else: value = self.strategy.broker.fundvalue - # update the maximum seen peak + # 更新已见到的最大峰值 if value > self.peak: self.peak = value - self.ddlen = 0 # start of streak + self.ddlen = 0 # streak 起点 - # calculate the current drawdown + # 计算当前 drawdown self.dd = dd = 100.0 * (self.peak - value) / self.peak - self.ddlen += bool(dd) # if peak == value -> dd = 0 + self.ddlen += bool(dd) # 如果 peak == value,则 dd = 0 - # update the maxdrawdown if needed + # 按需更新 maxdrawdown self.maxdd = max(self.maxdd, dd) self.maxddlen = max(self.maxddlen, self.ddlen) diff --git a/backtrader/analyzers/leverage.py b/backtrader/analyzers/leverage.py index 749362f00..9b8cdd553 100644 --- a/backtrader/analyzers/leverage.py +++ b/backtrader/analyzers/leverage.py @@ -25,26 +25,21 @@ class GrossLeverage(bt.Analyzer): - '''This analyzer calculates the Gross Leverage of the current strategy - on a timeframe basis + '''按时间点计算当前 strategy 的 Gross Leverage。 - Params: + Args: + fund: 如果为 ``None``,会自动检测 broker 的实际模式(fundmode - + True/False),以决定 leverage 基于总净资产 value 还是 fund + value。将其设为 ``True`` 或 ``False`` 可指定具体行为。 - - ``fund`` (default: ``None``) + Returns: + dict: ``get_analysis`` 返回以 datetime 为 key、Gross Leverage 为 + value 的字典。 - If ``None`` the actual mode of the broker (fundmode - True/False) will - be autodetected to decide if the returns are based on the total net - asset value or on the fund value. See ``set_fundmode`` in the broker - documentation - - Set it to ``True`` or ``False`` for a specific behavior - - Methods: - - - get_analysis - - Returns a dictionary with returns as values and the datetime points for - each return as keys + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addanalyzer(GrossLeverage, _name='grossleverage') ''' params = ( @@ -65,7 +60,7 @@ def notify_fund(self, cash, value, fundvalue, shares): self._value = fundvalue def next(self): - # Updates the leverage for "dtkey" (see base class) for each cycle - # 0.0 if 100% in cash, 1.0 if no short selling and fully invested + # 每个 cycle 更新 leverage + # 100% cash 时为 0.0;无 short selling 且满仓时为 1.0 lev = (self._value - self._cash) / self._value self.rets[self.data0.datetime.datetime()] = lev diff --git a/backtrader/analyzers/logreturnsrolling.py b/backtrader/analyzers/logreturnsrolling.py index 4dc8a8465..14724de60 100644 --- a/backtrader/analyzers/logreturnsrolling.py +++ b/backtrader/analyzers/logreturnsrolling.py @@ -31,66 +31,38 @@ class LogReturnsRolling(bt.TimeFrameAnalyzerBase): - '''This analyzer calculates rolling returns for a given timeframe and - compression - - Params: - - - ``timeframe`` (default: ``None``) - If ``None`` the ``timeframe`` of the 1st data in the system will be - used - - Pass ``TimeFrame.NoTimeFrame`` to consider the entire dataset with no - time constraints - - - ``compression`` (default: ``None``) - - Only used for sub-day timeframes to for example work on an hourly - timeframe by specifying "TimeFrame.Minutes" and 60 as compression - - If ``None`` then the compression of the 1st data of the system will be - used - - - ``data`` (default: ``None``) - - Reference asset to track instead of the portfolio value. - - .. note:: this data must have been added to a ``cerebro`` instance with - ``addata``, ``resampledata`` or ``replaydata`` - - - ``firstopen`` (default: ``True``) - - When tracking the returns of a ``data`` the following is done when - crossing a timeframe boundary, for example ``Years``: - - - Last ``close`` of previous year is used as the reference price to - see the return in the current year - - The problem is the 1st calculation, because the data has** no - previous** closing price. As such and when this parameter is ``True`` - the *opening* price will be used for the 1st calculation. - - This requires the data feed to have an ``open`` price (for ``close`` - the standard [0] notation will be used without reference to a field - price) - - Else the initial close will be used. - - - ``fund`` (default: ``None``) - - If ``None`` the actual mode of the broker (fundmode - True/False) will - be autodetected to decide if the returns are based on the total net - asset value or on the fund value. See ``set_fundmode`` in the broker - documentation - - Set it to ``True`` or ``False`` for a specific behavior - - Methods: - - - get_analysis - - Returns a dictionary with returns as values and the datetime points for - each return as keys + '''按给定 timeframe 和 compression 计算 rolling log return。 + + 该 analyzer 可以跟踪 portfolio/fund value,也可以跟踪指定 data 的价格变化。 + + Args: + timeframe: 统计使用的 timeframe,默认 ``None``。如果为 ``None``, + 使用系统中第 1 个 data 的 timeframe。传入 + ``TimeFrame.NoTimeFrame`` 可在不受时间约束的情况下考虑整个 + dataset。 + compression: timeframe 压缩倍数,默认 ``None``。仅用于日内 + timeframe。例如指定 ``TimeFrame.Minutes`` 并将 compression 设为 + 60,即可按小时 timeframe 工作。如果为 ``None``,使用系统中第 1 + 个 data 的 compression。 + data: 要跟踪的参考资产,默认 ``None``。如果为 ``None``,跟踪 + portfolio value 或 fund value。该 data 必须已经通过 + ``adddata``、``resampledata`` 或 ``replaydata`` 加入 ``cerebro``。 + firstopen (bool): 跟踪 data 且第 1 次计算没有前一根 close 时,是否用 + open 作为起始参考价,默认 ``True``。如果为 ``False``,使用初始 + close。 + fund: 如果为 ``None``,会自动检测 broker 的实际模式(fundmode - + True/False),以决定 returns 基于总净资产 value 还是 fund value。 + 将其设为 ``True`` 或 ``False`` 可指定具体行为。 + + Returns: + dict: ``get_analysis`` 返回以 datetime 为 key、rolling log return 为 + value 的字典。 + + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addanalyzer(LogReturnsRolling, timeframe=bt.TimeFrame.Months, + ... compression=3, _name='logreturnsrolling') ''' params = ( @@ -110,7 +82,7 @@ def start(self): maxlen=self.compression) if self.p.data is None: - # keep the initial portfolio value if not tracing a data + # 未跟踪 data 时,保留初始 portfolio value if not self._fundmode: self._lastvalue = self.strategy.broker.getvalue() else: @@ -123,18 +95,18 @@ def notify_fund(self, cash, value, fundvalue, shares): self._value = fundvalue if self.p.data is None else self.p.data[0] def _on_dt_over(self): - # next is called in a new timeframe period + # next 会在新的 timeframe period 中被调用 if self.p.data is None or len(self.p.data) > 1: - # Not tracking a data feed or data feed has data already - vst = self._lastvalue # update value_start to last + # 未跟踪 data feed,或 data feed 已经有数据 + vst = self._lastvalue # 将 value_start 更新为上次 value else: - # The 1st tick has no previous reference, use the opening price + # 第 1 个 tick 没有前值引用,使用 opening price vst = self.p.data.open[0] if self.p.firstopen else self.p.data[0] - self._values.append(vst) # push values backwards (and out) + self._values.append(vst) # 将 value 向后推入队列 def next(self): - # Calculate the return + # 计算 return super(LogReturnsRolling, self).next() self.rets[self.dtkey] = math.log(self._value / self._values[0]) - self._lastvalue = self._value # keep last value + self._lastvalue = self._value # 保留上次 value diff --git a/backtrader/analyzers/periodstats.py b/backtrader/analyzers/periodstats.py index 07271baec..3e17ab1b9 100644 --- a/backtrader/analyzers/periodstats.py +++ b/backtrader/analyzers/periodstats.py @@ -32,36 +32,25 @@ class PeriodStats(bt.Analyzer): - '''Calculates basic statistics for given timeframe - - Params: - - - ``timeframe`` (default: ``Years``) - If ``None`` the ``timeframe`` of the 1st data in the system will be - used - - Pass ``TimeFrame.NoTimeFrame`` to consider the entire dataset with no - time constraints - - - ``compression`` (default: ``1``) - - Only used for sub-day timeframes to for example work on an hourly - timeframe by specifying "TimeFrame.Minutes" and 60 as compression - - If ``None`` then the compression of the 1st data of the system will be - used - - - ``fund`` (default: ``None``) - - If ``None`` the actual mode of the broker (fundmode - True/False) will - be autodetected to decide if the returns are based on the total net - asset value or on the fund value. See ``set_fundmode`` in the broker - documentation - - Set it to ``True`` or ``False`` for a specific behavior - - - ``get_analysis`` returns a dictionary containing the keys: + '''计算给定 timeframe 的基础统计信息 + + Args: + timeframe: 统计使用的 timeframe,默认 ``Years``。如果为 ``None``, + 将使用系统中第 1 个 data 的 ``timeframe``。传入 + ``TimeFrame.NoTimeFrame`` 可在不受时间约束的情况下考虑整个 + dataset。 + compression (int): timeframe 压缩倍数,默认 ``1``。仅用于日内 + timeframe。例如指定 ``TimeFrame.Minutes`` 并将 compression 设为 + 60,即可按小时 timeframe 工作。如果为 ``None``,将使用系统中第 + 1 个 data 的 compression。 + zeroispos (bool): 如果设为 ``True``,无变化的 period 会被计为正数。 + fund: 如果为 ``None``,会自动检测 broker 的实际模式(fundmode - + True/False),以决定 returns 是基于总净资产 value 还是 fund + value。参见 broker 文档中的 ``set_fundmode``。将其设为 ``True`` + 或 ``False`` 可指定具体行为。 + + Returns: + dict: ``get_analysis`` 返回一个包含以下 key 的字典: - ``average`` - ``stddev`` @@ -71,8 +60,12 @@ class PeriodStats(bt.Analyzer): - ``best`` - ``worst`` - If the parameter ``zeroispos`` is set to ``True``, periods with no change - will be counted as positive + --- + 交互示例: + + >>> cerebro = bt.Cerebro() + >>> cerebro.addanalyzer(PeriodStats, timeframe=bt.TimeFrame.Years) + >>> # 运行后可通过 strategy.analyzers 中的 analyzer 调用 get_analysis() ''' params = ( diff --git a/backtrader/analyzers/positions.py b/backtrader/analyzers/positions.py index 7d2e1d7c4..bb5020d27 100644 --- a/backtrader/analyzers/positions.py +++ b/backtrader/analyzers/positions.py @@ -26,39 +26,28 @@ class PositionsValue(bt.Analyzer): - '''This analyzer reports the value of the positions of the current set of - datas - - Params: - - - timeframe (default: ``None``) - If ``None`` then the timeframe of the 1st data of the system will be - used - - - compression (default: ``None``) - - Only used for sub-day timeframes to for example work on an hourly - timeframe by specifying "TimeFrame.Minutes" and 60 as compression - - If ``None`` then the compression of the 1st data of the system will be - used - - - headers (default: ``False``) - - Add an initial key to the dictionary holding the results with the names - of the datas ('Datetime' as key - - - cash (default: ``False``) - - Include the actual cash as an extra position (for the header 'cash' - will be used as name) - - Methods: - - - get_analysis - - Returns a dictionary with returns as values and the datetime points for - each return as keys + '''报告当前所有 data position value 的 analyzer。 + + Args: + timeframe: 统计使用的 timeframe,默认 ``None``。如果为 ``None``, + 使用系统中第 1 个 data 的 timeframe。 + compression: timeframe 压缩倍数,默认 ``None``。仅用于日内 + timeframe。如果为 ``None``,使用系统中第 1 个 data 的 + compression。 + headers (bool): 是否在结果字典中添加一条初始 header,默认 + ``False``。header 使用 data 名称,key 为 ``Datetime``。 + cash (bool): 是否把当前 cash 作为额外 position 加入结果,默认 + ``False``。启用 header 时该列名为 ``cash``。 + + Returns: + dict: ``get_analysis`` 返回以 date/datetime 为 key、各 data position + value 列表为 value 的字典。 + + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addanalyzer(PositionsValue, headers=True, cash=True, + ... _name='positions') ''' params = ( ('headers', False), diff --git a/backtrader/analyzers/pyfolio.py b/backtrader/analyzers/pyfolio.py index d15bc9d04..891ba532e 100644 --- a/backtrader/analyzers/pyfolio.py +++ b/backtrader/analyzers/pyfolio.py @@ -31,52 +31,45 @@ class PyFolio(bt.Analyzer): - '''This analyzer uses 4 children analyzers to collect data and transforms it - in to a data set compatible with ``pyfolio`` + '''收集并转换为 ``pyfolio`` 兼容数据集的 analyzer。 - Children Analyzer + 该 analyzer 使用 4 个子 analyzer: - ``TimeReturn`` - Used to calculate the returns of the global portfolio value + 用于计算全局 portfolio value 的 returns - ``PositionsValue`` - Used to calculate the value of the positions per data. It sets the - ``headers`` and ``cash`` parameters to ``True`` + 用于计算每个 data 的 position value,并将 ``headers`` 和 ``cash`` 参数 + 设为 ``True`` - ``Transactions`` - Used to record each transaction on a data (size, price, value). Sets - the ``headers`` parameter to ``True`` + 用于记录每个 data 上的 transaction(size、price、value),并将 + ``headers`` 参数设为 ``True`` - ``GrossLeverage`` - Keeps track of the gross leverage (how much the strategy is invested) + 跟踪 gross leverage,即 strategy 已投入程度 - Params: - These are passed transparently to the children + Args: + timeframe: 传递给子 analyzer 的 timeframe,默认 ``bt.TimeFrame.Days``。 + 如果为 ``None``,使用系统中第 1 个 data 的 timeframe。 + compression: 传递给子 analyzer 的 compression,默认 ``1``。如果为 + ``None``,使用系统中第 1 个 data 的 compression。 - - timeframe (default: ``bt.TimeFrame.Days``) + Returns: + dict: ``get_analysis`` 返回包含 ``returns``、``positions``、 + ``transactions`` 和 ``gross_lev`` 的字典。 - If ``None`` then the timeframe of the 1st data of the system will be - used + ``timeframe`` 和 ``compression`` 的默认值遵循 ``pyfolio`` 的行为:使用 + daily data,并由 pyfolio 进一步 upsample 生成年度收益等结果。 - - compression (default: `1``) - - If ``None`` then the compression of the 1st data of the system will be - used - - Both ``timeframe`` and ``compression`` are set following the default - behavior of ``pyfolio`` which is working with *daily* data and upsample it - to obtaine values like yearly returns. - - Methods: - - - get_analysis - - Returns a dictionary with returns as values and the datetime points for - each return as keys + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addanalyzer(PyFolio, _name='pyfolio') ''' params = ( ('timeframe', bt.TimeFrame.Days), @@ -100,19 +93,19 @@ def stop(self): self.rets['gross_lev'] = self._gross_lev.get_analysis() def get_pf_items(self): - '''Returns a tuple of 4 elements which can be used for further processing with - ``pyfolio`` + '''返回可交给 ``pyfolio`` 继续处理的 4 元组。 - returns, positions, transactions, gross_leverage + Returns: + tuple: ``returns``、``positions``、``transactions``、 + ``gross_leverage``。 - Because the objects are meant to be used as direct input to ``pyfolio`` - this method makes a local import of ``pandas`` to convert the internal - *backtrader* results to *pandas DataFrames* which is the expected input - by, for example, ``pyfolio.create_full_tear_sheet`` + 因为这些对象会作为 ``pyfolio`` 的直接输入,本方法会局部导入 + ``pandas``,把内部 *backtrader* 结果转换成 *pandas DataFrames*。 + 这是例如 ``pyfolio.create_full_tear_sheet`` 所期望的输入格式。 - The method will break if ``pandas`` is not installed + 如果未安装 ``pandas``,该方法会失败。 ''' - # keep import local to avoid disturbing installations with no pandas + # 保持局部导入,避免影响未安装 pandas 的环境 import pandas from pandas import DataFrame as DF @@ -128,7 +121,7 @@ def get_pf_items(self): # Positions pss = self.rets['positions'] ps = [[k] + v[-2:] for k, v in iteritems(pss)] - cols = ps.pop(0) # headers are in the first entry + cols = ps.pop(0) # headers 位于第 1 条记录 positions = DF.from_records(ps, index=cols[0], columns=cols) positions.index = pandas.to_datetime(positions.index) positions.index = positions.index.tz_localize('UTC') @@ -137,15 +130,14 @@ def get_pf_items(self): # Transactions txss = self.rets['transactions'] txs = list() - # The transactions have a common key (date) and can potentially happend - # for several assets. The dictionary has a single key and a list of - # lists. Each sublist contains the fields of a transaction - # Hence the double loop to undo the list indirection + # transactions 有公共 key(date),并且可能发生在多个 asset 上。 + # 字典中一个 key 对应一个 list of lists;每个子 list 包含一条 + # transaction 的字段,因此需要双层循环展开 list 间接层 for k, v in iteritems(txss): for v2 in v: txs.append([k] + v2) - cols = txs.pop(0) # headers are in the first entry + cols = txs.pop(0) # headers 位于第 1 条记录 transactions = DF.from_records(txs, index=cols[0], columns=cols) transactions.index = pandas.to_datetime(transactions.index) transactions.index = transactions.index.tz_localize('UTC') @@ -159,5 +151,5 @@ def get_pf_items(self): gross_lev.index = gross_lev.index.tz_localize('UTC') glev = gross_lev['gross_lev'] - # Return all together + # 一起返回 return rets, positions, transactions, glev diff --git a/backtrader/analyzers/returns.py b/backtrader/analyzers/returns.py index ceb39e31e..27710c0d0 100644 --- a/backtrader/analyzers/returns.py +++ b/backtrader/analyzers/returns.py @@ -28,64 +28,41 @@ class Returns(TimeFrameAnalyzerBase): - '''Total, Average, Compound and Annualized Returns calculated using a - logarithmic approach + '''使用 logarithmic 方法计算总收益、平均收益、复合收益和年化收益。 - See: + 参考: - https://www.crystalbull.com/sharpe-ratio-better-with-log-returns/ - Params: - - - ``timeframe`` (default: ``None``) - - If ``None`` the ``timeframe`` of the 1st data in the system will be - used - - Pass ``TimeFrame.NoTimeFrame`` to consider the entire dataset with no - time constraints - - - ``compression`` (default: ``None``) - - Only used for sub-day timeframes to for example work on an hourly - timeframe by specifying "TimeFrame.Minutes" and 60 as compression - - If ``None`` then the compression of the 1st data of the system will be - used - - - ``tann`` (default: ``None``) - - Number of periods to use for the annualization (normalization) of the - - namely: - - - ``days: 252`` - - ``weeks: 52`` - - ``months: 12`` - - ``years: 1`` - - - ``fund`` (default: ``None``) - - If ``None`` the actual mode of the broker (fundmode - True/False) will - be autodetected to decide if the returns are based on the total net - asset value or on the fund value. See ``set_fundmode`` in the broker - documentation - - Set it to ``True`` or ``False`` for a specific behavior - - Methods: - - - get_analysis - - Returns a dictionary with returns as values and the datetime points for - each return as keys - - The returned dict the following keys: - - - ``rtot``: Total compound return - - ``ravg``: Average return for the entire period (timeframe specific) - - ``rnorm``: Annualized/Normalized return - - ``rnorm100``: Annualized/Normalized return expressed in 100% + Args: + timeframe: 统计使用的 timeframe,默认 ``None``。如果为 ``None``, + 使用系统中第 1 个 data 的 timeframe。传入 + ``TimeFrame.NoTimeFrame`` 可在不受时间约束的情况下考虑整个 + dataset。 + compression: timeframe 压缩倍数,默认 ``None``。仅用于日内 + timeframe。例如指定 ``TimeFrame.Minutes`` 并将 compression 设为 + 60,即可按小时 timeframe 工作。如果为 ``None``,使用系统中第 1 + 个 data 的 compression。 + tann: 年化(normalization)使用的 period 数量,默认 ``None``。如果 + 未指定,会按 timeframe 使用默认值: days=252、weeks=52、 + months=12、years=1。 + fund: 如果为 ``None``,会自动检测 broker 的实际模式(fundmode - + True/False),以决定 returns 基于总净资产 value 还是 fund value。 + 将其设为 ``True`` 或 ``False`` 可指定具体行为。 + + Returns: + dict: ``get_analysis`` 返回包含以下 key 的字典: + + - ``rtot``: 总复合 return + - ``ravg``: 整个 period 的平均 return(与 timeframe 相关) + - ``rnorm``: 年化/标准化 return + - ``rnorm100``: 以 100% 表示的年化/标准化 return + + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addanalyzer(Returns, timeframe=bt.TimeFrame.Years, + ... _name='returns') ''' @@ -123,7 +100,7 @@ def stop(self): else: self._value_end = self.strategy.broker.fundvalue - # Compound return + # 复合 return try: nlrtot = self._value_end / self._value_start except ZeroDivisionError: @@ -136,20 +113,20 @@ def stop(self): self.rets['rtot'] = rtot - # Average return + # 平均 return self.rets['ravg'] = ravg = rtot / self._tcount - # Annualized normalized return + # 年化标准化 return tann = self.p.tann or self._TANN.get(self.timeframe, None) if tann is None: - tann = self._TANN.get(self.data._timeframe, 1.0) # assign default + tann = self._TANN.get(self.data._timeframe, 1.0) # 指派默认值 if ravg > float('-inf'): self.rets['rnorm'] = rnorm = math.expm1(ravg * tann) else: self.rets['rnorm'] = rnorm = ravg - self.rets['rnorm100'] = rnorm * 100.0 # human readable % + self.rets['rnorm100'] = rnorm * 100.0 # 更便于阅读的百分比 def _on_dt_over(self): - self._tcount += 1 # count the subperiod + self._tcount += 1 # 统计 subperiod diff --git a/backtrader/analyzers/sharpe.py b/backtrader/analyzers/sharpe.py index d6971c636..2c0cb96b0 100644 --- a/backtrader/analyzers/sharpe.py +++ b/backtrader/analyzers/sharpe.py @@ -31,82 +31,40 @@ class SharpeRatio(Analyzer): - '''This analyzer calculates the SharpeRatio of a strategy using a risk free - asset which is simply an interest rate + '''使用 risk-free rate 计算 strategy Sharpe Ratio 的 analyzer。 See also: - https://en.wikipedia.org/wiki/Sharpe_ratio - Params: - - - ``timeframe``: (default: ``TimeFrame.Years``) - - - ``compression`` (default: ``1``) - - Only used for sub-day timeframes to for example work on an hourly - timeframe by specifying "TimeFrame.Minutes" and 60 as compression - - - ``riskfreerate`` (default: 0.01 -> 1%) - - Expressed in annual terms (see ``convertrate`` below) - - - ``convertrate`` (default: ``True``) - - Convert the ``riskfreerate`` from annual to monthly, weekly or daily - rate. Sub-day conversions are not supported - - - ``factor`` (default: ``None``) - - If ``None``, the conversion factor for the riskfree rate from *annual* - to the chosen timeframe will be chosen from a predefined table - - Days: 252, Weeks: 52, Months: 12, Years: 1 - - Else the specified value will be used - - - ``annualize`` (default: ``False``) - - If ``convertrate`` is ``True``, the *SharpeRatio* will be delivered in - the ``timeframe`` of choice. - - In most occasions the SharpeRatio is delivered in annualized form. - Convert the ``riskfreerate`` from annual to monthly, weekly or daily - rate. Sub-day conversions are not supported - - - ``stddev_sample`` (default: ``False``) - - If this is set to ``True`` the *standard deviation* will be calculated - decreasing the denominator in the mean by ``1``. This is used when - calculating the *standard deviation* if it's considered that not all - samples are used for the calculation. This is known as the *Bessels' - correction* - - - ``daysfactor`` (default: ``None``) - - Old naming for ``factor``. If set to anything else than ``None`` and - the ``timeframe`` is ``TimeFrame.Days`` it will be assumed this is old - code and the value will be used - - - ``legacyannual`` (default: ``False``) - - Use the ``AnnualReturn`` return analyzer, which as the name implies - only works on years - - - ``fund`` (default: ``None``) - - If ``None`` the actual mode of the broker (fundmode - True/False) will - be autodetected to decide if the returns are based on the total net - asset value or on the fund value. See ``set_fundmode`` in the broker - documentation - - Set it to ``True`` or ``False`` for a specific behavior - - Methods: - - - get_analysis - - Returns a dictionary with key "sharperatio" holding the ratio + Args: + timeframe: 统计使用的 timeframe,默认 ``TimeFrame.Years``。 + compression (int): timeframe 压缩倍数,默认 ``1``。仅用于日内 + timeframe。 + riskfreerate (float): 年化 risk-free rate,默认 ``0.01``(1%)。 + factor: annual risk-free rate 到所选 timeframe 的转换因子,默认 + ``None``。如果为 ``None``,会从预设表中选择: + Days=252、Weeks=52、Months=12、Years=1。 + convertrate (bool): 是否将 ``riskfreerate`` 从年化转换为月/周/日 + rate,默认 ``True``。不支持日内转换。 + annualize (bool): 是否返回年化 Sharpe Ratio,默认 ``False``。 + stddev_sample (bool): 是否使用样本标准差,默认 ``False``。设为 + ``True`` 时使用 Bessel's correction。 + daysfactor: ``factor`` 的旧名称,默认 ``None``。如果 timeframe 是 + ``TimeFrame.Days`` 且该参数不为 ``None``,会按旧代码行为使用它。 + legacyannual (bool): 是否使用 ``AnnualReturn`` analyzer,默认 + ``False``。该 analyzer 只按年份工作。 + fund: 如果为 ``None``,会自动检测 broker 的实际模式(fundmode - + True/False),以决定 returns 基于总净资产 value 还是 fund value。 + 将其设为 ``True`` 或 ``False`` 可指定具体行为。 + + Returns: + dict: ``get_analysis`` 返回包含 ``sharperatio`` key 的字典。 + + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addanalyzer(SharpeRatio, _name='sharpe') ''' params = ( @@ -118,7 +76,7 @@ class SharpeRatio(Analyzer): ('annualize', False), ('stddev_sample', False), - # old behavior + # 旧行为 ('daysfactor', None), ('legacyannual', False), ('fund', None), @@ -149,14 +107,14 @@ def stop(self): self.ratio = retavg / retdev else: - # Get the returns from the subanalyzer + # 从子 analyzer 获取 returns returns = list(itervalues(self.timereturn.get_analysis())) rate = self.p.riskfreerate # factor = None - # Hack to identify old code + # 用于识别旧代码的兼容逻辑 if self.p.timeframe == TimeFrame.Days and \ self.p.daysfactor is not None: @@ -164,25 +122,25 @@ def stop(self): else: if self.p.factor is not None: - factor = self.p.factor # user specified factor + factor = self.p.factor # 用户指定的 factor elif self.p.timeframe in self.RATEFACTORS: - # Get the conversion factor from the default table + # 从默认表中获取转换 factor factor = self.RATEFACTORS[self.p.timeframe] if factor is not None: - # A factor was found + # 找到 factor if self.p.convertrate: - # Standard: downgrade annual returns to timeframe factor + # 标准做法:将年化 return 降频到 timeframe factor rate = pow(1.0 + rate, 1.0 / factor) - 1.0 else: - # Else upgrade returns to yearly returns + # 否则将 returns 升频到年度 returns returns = [pow(1.0 + x, factor) - 1.0 for x in returns] lrets = len(returns) - self.p.stddev_sample - # Check if the ratio can be calculated + # 检查 ratio 是否可计算 if lrets: - # Get the excess returns - arithmetic mean - original sharpe + # 计算 excess returns、算术平均和原始 sharpe ret_free = [r - rate for r in returns] ret_free_avg = average(ret_free) retdev = standarddev(ret_free, avgx=ret_free_avg, @@ -198,7 +156,7 @@ def stop(self): except (ValueError, TypeError, ZeroDivisionError): ratio = None else: - # no returns or stddev_sample was active and 1 return + # 无 returns,或 stddev_sample 启用且只有 1 个 return ratio = None self.ratio = ratio @@ -207,12 +165,18 @@ def stop(self): class SharpeRatio_A(SharpeRatio): - '''Extension of the SharpeRatio which returns the Sharpe Ratio directly in - annualized form + '''直接返回年化 Sharpe Ratio 的 ``SharpeRatio`` 扩展类。 + + Args: + annualize (bool): 默认改为 ``True``。 - The following param has been changed from ``SharpeRatio`` + Returns: + dict: ``get_analysis`` 返回包含 ``sharperatio`` key 的字典。 - - ``annualize`` (default: ``True``) + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addanalyzer(SharpeRatio_A, _name='sharpe_annual') ''' diff --git a/backtrader/analyzers/sqn.py b/backtrader/analyzers/sqn.py index 56732f856..87557405f 100644 --- a/backtrader/analyzers/sqn.py +++ b/backtrader/analyzers/sqn.py @@ -29,8 +29,9 @@ class SQN(Analyzer): - '''SQN or SystemQualityNumber. Defined by Van K. Tharp to categorize trading - systems. + '''计算 SQN(System Quality Number)的 analyzer。 + + SQN 由 Van K. Tharp 定义,用于给交易系统分类。 - 1.6 - 1.9 Below average - 2.0 - 2.4 Average @@ -39,25 +40,35 @@ class SQN(Analyzer): - 5.1 - 6.9 Superb - 7.0 - Holy Grail? - The formula: + 公式: - SquareRoot(NumberTrades) * Average(TradesProfit) / StdDev(TradesProfit) - The sqn value should be deemed reliable when the number of trades >= 30 + 当 trade 数量 >= 30 时,SQN 值通常更可靠。 + + Args: + 无。 - Methods: + Returns: + AutoOrderedDict: ``get_analysis`` 返回包含以下 key 的对象: - - get_analysis + - ``sqn``: 计算得到的 SQN + - ``trades``: 纳入计算的 trade 数量 - Returns a dictionary with keys "sqn" and "trades" (number of - considered trades) + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addanalyzer(SQN, _name='sqn') ''' alias = ('SystemQualityNumber',) def create_analysis(self): - '''Replace default implementation to instantiate an AutoOrdereDict - rather than an OrderedDict''' + '''使用 ``AutoOrderedDict`` 替代默认 ``OrderedDict``。 + + Returns: + None: 该方法只初始化 ``self.rets``。 + ''' self.rets = AutoOrderedDict() def start(self): diff --git a/backtrader/analyzers/timereturn.py b/backtrader/analyzers/timereturn.py index 70374e368..1f67e780d 100644 --- a/backtrader/analyzers/timereturn.py +++ b/backtrader/analyzers/timereturn.py @@ -25,66 +25,38 @@ class TimeReturn(TimeFrameAnalyzerBase): - '''This analyzer calculates the Returns by looking at the beginning - and end of the timeframe - - Params: - - - ``timeframe`` (default: ``None``) - If ``None`` the ``timeframe`` of the 1st data in the system will be - used - - Pass ``TimeFrame.NoTimeFrame`` to consider the entire dataset with no - time constraints - - - ``compression`` (default: ``None``) - - Only used for sub-day timeframes to for example work on an hourly - timeframe by specifying "TimeFrame.Minutes" and 60 as compression - - If ``None`` then the compression of the 1st data of the system will be - used - - - ``data`` (default: ``None``) - - Reference asset to track instead of the portfolio value. - - .. note:: this data must have been added to a ``cerebro`` instance with - ``addata``, ``resampledata`` or ``replaydata`` - - - ``firstopen`` (default: ``True``) - - When tracking the returns of a ``data`` the following is done when - crossing a timeframe boundary, for example ``Years``: - - - Last ``close`` of previous year is used as the reference price to - see the return in the current year - - The problem is the 1st calculation, because the data has** no - previous** closing price. As such and when this parameter is ``True`` - the *opening* price will be used for the 1st calculation. - - This requires the data feed to have an ``open`` price (for ``close`` - the standard [0] notation will be used without reference to a field - price) - - Else the initial close will be used. - - - ``fund`` (default: ``None``) - - If ``None`` the actual mode of the broker (fundmode - True/False) will - be autodetected to decide if the returns are based on the total net - asset value or on the fund value. See ``set_fundmode`` in the broker - documentation - - Set it to ``True`` or ``False`` for a specific behavior - - Methods: - - - get_analysis - - Returns a dictionary with returns as values and the datetime points for - each return as keys + '''按指定 timeframe 计算 return 的 analyzer。 + + 该 analyzer 会比较每个 timeframe 起点和终点的 value,既可以跟踪 + portfolio/fund value,也可以跟踪指定 data 的价格变化。 + + Args: + timeframe: 统计使用的 timeframe,默认 ``None``。如果为 ``None``, + 使用系统中第 1 个 data 的 timeframe。传入 + ``TimeFrame.NoTimeFrame`` 可在不受时间约束的情况下考虑整个 + dataset。 + compression: timeframe 压缩倍数,默认 ``None``。仅用于日内 + timeframe。例如指定 ``TimeFrame.Minutes`` 并将 compression 设为 + 60,即可按小时 timeframe 工作。如果为 ``None``,使用系统中第 1 + 个 data 的 compression。 + data: 要跟踪的参考资产,默认 ``None``。如果为 ``None``,跟踪 + portfolio value 或 fund value。该 data 必须已经通过 + ``adddata``、``resampledata`` 或 ``replaydata`` 加入 ``cerebro``。 + firstopen (bool): 跟踪 data 且第 1 次计算没有前一根 close 时,是否用 + open 作为起始参考价,默认 ``True``。如果为 ``False``,使用初始 + close。 + fund: 如果为 ``None``,会自动检测 broker 的实际模式(fundmode - + True/False),以决定 returns 基于总净资产 value 还是 fund value。 + 将其设为 ``True`` 或 ``False`` 可指定具体行为。 + + Returns: + dict: ``get_analysis`` 返回以 datetime 为 key、return 为 value 的字典。 + + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addanalyzer(TimeReturn, timeframe=bt.TimeFrame.Months, + ... _name='timereturn') ''' params = ( @@ -103,7 +75,7 @@ def start(self): self._value_start = 0.0 self._lastvalue = None if self.p.data is None: - # keep the initial portfolio value if not tracing a data + # 未跟踪 data 时,保留初始 portfolio value if not self._fundmode: self._lastvalue = self.strategy.broker.getvalue() else: @@ -111,32 +83,32 @@ def start(self): def notify_fund(self, cash, value, fundvalue, shares): if not self._fundmode: - # Record current value + # 记录当前 value if self.p.data is None: - self._value = value # the portofolio value if tracking no data + self._value = value # 未跟踪 data 时为 portfolio value else: - self._value = self.p.data[0] # the data value if tracking data + self._value = self.p.data[0] # 跟踪 data 时为 data value else: if self.p.data is None: - self._value = fundvalue # the fund value if tracking no data + self._value = fundvalue # 未跟踪 data 时为 fund value else: - self._value = self.p.data[0] # the data value if tracking data + self._value = self.p.data[0] # 跟踪 data 时为 data value def on_dt_over(self): - # next is called in a new timeframe period + # next 会在新的 timeframe period 中被调用 # if self.p.data is None or len(self.p.data) > 1: if self.p.data is None or self._lastvalue is not None: - self._value_start = self._lastvalue # update value_start to last + self._value_start = self._lastvalue # 将 value_start 更新为上次 value else: - # The 1st tick has no previous reference, use the opening price + # 第 1 个 tick 没有前值引用,使用 opening price if self.p.firstopen: self._value_start = self.p.data.open[0] else: self._value_start = self.p.data[0] def next(self): - # Calculate the return + # 计算 return super(TimeReturn, self).next() self.rets[self.dtkey] = (self._value / self._value_start) - 1.0 - self._lastvalue = self._value # keep last value + self._lastvalue = self._value # 保留上次 value diff --git a/backtrader/analyzers/tradeanalyzer.py b/backtrader/analyzers/tradeanalyzer.py index 2714d0124..ad3a58dfc 100644 --- a/backtrader/analyzers/tradeanalyzer.py +++ b/backtrader/analyzers/tradeanalyzer.py @@ -29,8 +29,9 @@ class TradeAnalyzer(Analyzer): - ''' - Provides statistics on closed trades (keeps also the count of open ones) + '''统计 closed trades,并同时保留 open trades 数量的 analyzer。 + + 统计内容包括: - Total Open/Closed Trades @@ -38,13 +39,13 @@ class TradeAnalyzer(Analyzer): - ProfitAndLoss Total/Average - - Won/Lost Count/ Total PNL/ Average PNL / Max PNL + - Won/Lost Count / Total PNL / Average PNL / Max PNL - - Long/Short Count/ Total PNL / Average PNL / Max PNL + - Long/Short Count / Total PNL / Average PNL / Max PNL - - Won/Lost Count/ Total PNL/ Average PNL / Max PNL + - Won/Lost Count / Total PNL / Average PNL / Max PNL - - Length (bars in the market) + - Length(持仓 bar 数) - Total/Average/Max/Min @@ -54,16 +55,20 @@ class TradeAnalyzer(Analyzer): - Won/Lost Total/Average/Max/Min - Note: + Args: + 无。 - The analyzer uses an "auto"dict for the fields, which means that if no - trades are executed, no statistics will be generated. + Returns: + AutoOrderedDict: ``get_analysis`` 返回支持 ``.`` 访问的统计对象。 - In that case there will be a single field/subfield in the dictionary - returned by ``get_analysis``, namely: + Note: + 该 analyzer 使用 "auto" dict 存放字段。如果没有执行任何 trade,就不会 + 生成完整统计项;此时返回字典中只有 ``total.total``,其值为 ``0``。 - - dictname['total']['total'] which will have a value of 0 (the field is - also reachable with dot notation dictname.total.total + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addanalyzer(TradeAnalyzer, _name='trades') ''' def create_analysis(self): self.rets = AutoOrderedDict() @@ -75,7 +80,7 @@ def stop(self): def notify_trade(self, trade): if trade.justopened: - # Trade just opened + # trade 刚刚打开 self.rets.total.total += 1 self.rets.total.open += 1 @@ -83,7 +88,7 @@ def notify_trade(self, trade): trades = self.rets res = AutoDict() - # Trade just closed + # trade 刚刚关闭 won = res.won = int(trade.pnlcomm >= 0.0) lost = res.lost = int(not won) @@ -93,7 +98,7 @@ def notify_trade(self, trade): trades.total.open -= 1 trades.total.closed += 1 - # Streak + # 连胜/连败 streak for wlname in ['won', 'lost']: wl = res[wlname] @@ -110,7 +115,7 @@ def notify_trade(self, trade): trpnl.net.total += trade.pnlcomm trpnl.net.average = trades.pnl.net.total / trades.total.closed - # Won/Lost statistics + # Won/Lost 统计 for wlname in ['won', 'lost']: wl = res[wlname] trwl = trades[wlname] @@ -127,7 +132,7 @@ def notify_trade(self, trade): func = max if wlname == 'won' else min trwlpnl.max = func(wm, pnlcomm) - # Long/Short statistics + # Long/Short 统计 for tname in ['long', 'short']: trls = trades[tname] ls = res['t' + tname] diff --git a/backtrader/analyzers/transactions.py b/backtrader/analyzers/transactions.py index dbeb94e94..43bc33b28 100644 --- a/backtrader/analyzers/transactions.py +++ b/backtrader/analyzers/transactions.py @@ -29,33 +29,24 @@ class Transactions(bt.Analyzer): - '''This analyzer reports the transactions occurred with each an every data in - the system + '''报告系统中每个 data 发生的 transaction。 - It looks at the order execution bits to create a ``Position`` starting from - 0 during each ``next`` cycle. + 该 analyzer 会读取 order execution bits,并在每个 ``next`` cycle 中从 0 + 开始构造临时 ``Position``,用来汇总该 cycle 内发生的 transaction。 - The result is used during next to record the transactions + Args: + headers (bool): 是否在结果字典中添加初始 header,默认 ``False``。 + _pfheaders (tuple): pyfolio 风格的 header 名称,默认包含 + ``date``、``amount``、``price``、``sid``、``symbol``、``value``。 - Params: + Returns: + dict: ``get_analysis`` 返回以 datetime 为 key、transaction 列表为 + value 的字典。 - - headers (default: ``True``) - - Add an initial key to the dictionary holding the results with the names - of the datas - - This analyzer was modeled to facilitate the integration with - ``pyfolio`` and the header names are taken from the samples used for - it:: - - 'date', 'amount', 'price', 'sid', 'symbol', 'value' - - Methods: - - - get_analysis - - Returns a dictionary with returns as values and the datetime points for - each return as keys + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addanalyzer(Transactions, headers=True, _name='transactions') ''' params = ( ('headers', False), @@ -71,24 +62,22 @@ def start(self): self._idnames = list(enumerate(self.strategy.getdatanames())) def notify_order(self, order): - # An order could have several partial executions per cycle (unlikely - # but possible) and therefore: collect each new execution notification - # and let the work for next + # 一个 order 在单个 cycle 中可能有多次 partial execution(少见但可能) + # 因此先收集每个新的 execution notification,把汇总留给 next - # We use a fresh Position object for each round to get summary of what - # the execution bits have done in that round + # 每轮使用新的 Position 对象,汇总本轮 execution bits 的效果 if order.status not in [Order.Partial, Order.Completed]: - return # It's not an execution + return # 不是 execution pos = self._positions[order.data._name] for exbit in order.executed.iterpending(): if exbit is None: - break # end of pending reached + break # 已到达 pending 末尾 pos.update(exbit.size, exbit.price) def next(self): - # super(Transactions, self).next() # let dtkey update + # super(Transactions, self).next() # 让 dtkey 更新 entries = [] for i, dname in self._idnames: pos = self._positions.get(dname, None) diff --git a/backtrader/analyzers/vwr.py b/backtrader/analyzers/vwr.py index 2bd758aed..6916585b9 100644 --- a/backtrader/analyzers/vwr.py +++ b/backtrader/analyzers/vwr.py @@ -30,71 +30,42 @@ class VWR(TimeFrameAnalyzerBase): - '''Variability-Weighted Return: Better SharpeRatio with Log Returns + '''计算 VWR(Variability-Weighted Return)的 analyzer。 + + VWR 可以理解为使用 Log Returns 的改进型 SharpeRatio。 Alias: - VariabilityWeightedReturn - See: + 参考: - https://www.crystalbull.com/sharpe-ratio-better-with-log-returns/ - Params: - - - ``timeframe`` (default: ``None``) - If ``None`` then the complete return over the entire backtested period - will be reported - - Pass ``TimeFrame.NoTimeFrame`` to consider the entire dataset with no - time constraints - - - ``compression`` (default: ``None``) - - Only used for sub-day timeframes to for example work on an hourly - timeframe by specifying "TimeFrame.Minutes" and 60 as compression - - If ``None`` then the compression of the 1st data of the system will be - used - - - ``tann`` (default: ``None``) - - Number of periods to use for the annualization (normalization) of the - average returns. If ``None``, then standard ``t`` values will be used, - namely: - - - ``days: 252`` - - ``weeks: 52`` - - ``months: 12`` - - ``years: 1`` - - - ``tau`` (default: ``2.0``) - - factor for the calculation (see the literature) - - - ``sdev_max`` (default: ``0.20``) - - max standard deviation (see the literature) - - - ``fund`` (default: ``None``) - - If ``None`` the actual mode of the broker (fundmode - True/False) will - be autodetected to decide if the returns are based on the total net - asset value or on the fund value. See ``set_fundmode`` in the broker - documentation - - Set it to ``True`` or ``False`` for a specific behavior - - Methods: - - - get_analysis - - Returns a dictionary with returns as values and the datetime points for - each return as keys - - The returned dict contains the following keys: - - - ``vwr``: Variability-Weighted Return + Args: + timeframe: 统计使用的 timeframe,默认 ``None``。如果为 ``None``, + 报告整个 backtest period 的完整 return。传入 + ``TimeFrame.NoTimeFrame`` 可在不受时间约束的情况下考虑整个 + dataset。 + compression: timeframe 压缩倍数,默认 ``None``。仅用于日内 + timeframe。如果为 ``None``,使用系统中第 1 个 data 的 + compression。 + tann: 年化(normalization)平均 return 使用的 period 数量,默认 + ``None``。如果为 ``None``,会使用标准值: + days=252、weeks=52、months=12、years=1。 + tau (float): 计算使用的 factor,默认 ``0.20``。 + sdev_max (float): 最大 standard deviation,默认 ``2.0``。 + fund: 如果为 ``None``,会自动检测 broker 的实际模式(fundmode - + True/False),以决定 returns 基于总净资产 value 还是 fund value。 + 将其设为 ``True`` 或 ``False`` 可指定具体行为。 + + Returns: + dict: ``get_analysis`` 返回包含 ``vwr`` key 的字典。 + + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addanalyzer(VWR, _name='vwr') ''' params = ( @@ -112,41 +83,41 @@ class VWR(TimeFrameAnalyzerBase): } def __init__(self): - # Children log return analyzer + # 子 log return analyzer self._returns = Returns(timeframe=self.p.timeframe, compression=self.p.compression, tann=self.p.tann) def start(self): super(VWR, self).start() - # Add an initial placeholder for [-1] operation + # 为 [-1] 操作添加初始占位 if self.p.fund is None: self._fundmode = self.strategy.broker.fundmode else: self._fundmode = self.p.fund if not self._fundmode: - self._pis = [self.strategy.broker.getvalue()] # keep initial value + self._pis = [self.strategy.broker.getvalue()] # 保留初始 value else: - self._pis = [self.strategy.broker.fundvalue] # keep initial value + self._pis = [self.strategy.broker.fundvalue] # 保留初始 value - self._pns = [None] # keep final prices (value) + self._pns = [None] # 保留最终 price/value def stop(self): super(VWR, self).stop() - # Check if no value has been seen after the last 'dt_over' - # If so, there is one 'pi' out of place and a None 'pn'. Purge + # 检查最后一次 dt_over 之后是否没有看到 value + # 如果是,则会多出一个错位的 pi 和一个 None pn,需要清理 if self._pns[-1] is None: self._pis.pop() self._pns.pop() - # Get results from children + # 从子 analyzer 获取结果 rs = self._returns.get_analysis() ravg = rs['ravg'] rnorm100 = rs['rnorm100'] - # make n 1 based in enumerate (number of periods and not index) - # skip initial placeholders for synchronization + # 让 enumerate 中的 n 从 1 开始(表示 period 数量而不是 index) + # 跳过用于同步的初始占位 dts = [] for n, pipn in enumerate(zip(self._pis, self._pns), 1): pi, pn = pipn @@ -161,13 +132,13 @@ def stop(self): def notify_fund(self, cash, value, fundvalue, shares): if not self._fundmode: - self._pns[-1] = value # annotate last seen pn for current period + self._pns[-1] = value # 标注当前 period 最后看到的 pn else: - self._pns[-1] = fundvalue # annotate last pn for current period + self._pns[-1] = fundvalue # 标注当前 period 的最后 pn def _on_dt_over(self): - self._pis.append(self._pns[-1]) # last pn is pi in next period - self._pns.append(None) # placeholder for [-1] operation + self._pis.append(self._pns[-1]) # 上一个 pn 是下一个 period 的 pi + self._pns.append(None) # [-1] 操作用占位 VariabilityWeightedReturn = VWR diff --git a/backtrader/broker.py b/backtrader/broker.py index b88de5b21..da37d38c6 100644 --- a/backtrader/broker.py +++ b/backtrader/broker.py @@ -32,9 +32,9 @@ class MetaBroker(MetaParams): def __init__(cls, name, bases, dct): ''' - Class has already been created ... fill missing methods if needed be + 类已经创建完成;按需补齐缺失方法。 ''' - # Initialize the class + # 初始化类 super(MetaBroker, cls).__init__(name, bases, dct) translations = { 'get_cash': 'getcash', @@ -47,6 +47,13 @@ def __init__(cls, name, bases, dct): class BrokerBase(with_metaclass(MetaBroker, object)): + '''Broker 的基类,用于定义 broker 实现需要提供的基础接口。 + + 该类负责保存 commission scheme,并定义 cash/value、position、submit、 + cancel、buy、sell 等 broker 行为的抽象接口。具体 broker 子类需要覆盖 + 未实现的方法。 + ''' + params = ( ('commission', CommInfoBase(percabs=True)), ) @@ -56,7 +63,7 @@ def __init__(self): self.init() def init(self): - # called from init and from start + # 从 init 和 start 中调用 if None not in self.comminfo: self.comminfo = dict({None: self.p.commission}) @@ -67,16 +74,36 @@ def stop(self): pass def add_order_history(self, orders, notify=False): - '''Add order history. See cerebro for details''' + '''添加 order history。 + + Args: + orders: order history 数据。 + notify (bool): 是否发送通知。 + + 详情参见 cerebro。 + ''' raise NotImplementedError def set_fund_history(self, fund): - '''Add fund history. See cerebro for details''' + '''添加 fund history。 + + Args: + fund: fund history 数据。 + + 详情参见 cerebro。 + ''' raise NotImplementedError def getcommissioninfo(self, data): - '''Retrieves the ``CommissionInfo`` scheme associated with the given - ``data``''' + '''获取与给定 ``data`` 关联的 ``CommissionInfo`` scheme。 + + Args: + data: 需要查询 commission scheme 的 data。 + + Returns: + CommInfoBase: 与 data 关联的 commission scheme;如果没有 data 专属 + scheme,则返回默认 scheme。 + ''' if data._name in self.comminfo: return self.comminfo[data._name] @@ -89,12 +116,23 @@ def setcommission(self, automargin=False, name=None): - '''This method sets a `` CommissionInfo`` object for assets managed in - the broker with the parameters. Consult the reference for - ``CommInfoBase`` - - If name is ``None``, this will be the default for assets for which no - other ``CommissionInfo`` scheme can be found + '''使用参数为 broker 管理的资产设置 ``CommissionInfo`` 对象。 + + Args: + commission (float): commission 数值。 + margin: margin 设置。 + mult (float): asset value/profit 乘数。 + commtype: commission 类型。 + percabs (bool): 百分比 commission 是否按 0.XX 理解。 + stocklike (bool): 是否按 stock-like 行为处理。 + interest (float): 年化 credit interest。 + interest_long (bool): long position 是否也收取 interest。 + leverage (float): leverage 值。 + automargin: 自动 margin 规则。 + name: data 名称。如果为 ``None``,则作为没有专属 scheme 的资产的 + 默认 scheme。 + + 详情参见 ``CommInfoBase``。 ''' comm = CommInfoBase(commission=commission, margin=margin, mult=mult, @@ -105,8 +143,13 @@ def setcommission(self, self.comminfo[name] = comm def addcommissioninfo(self, comminfo, name=None): - '''Adds a ``CommissionInfo`` object that will be the default for all assets if - ``name`` is ``None``''' + '''添加 ``CommissionInfo`` 对象。 + + Args: + comminfo: 要添加的 commission info 对象。 + name: data 名称。如果为 ``None``,则该对象作为所有资产的默认 + commission info。 + ''' self.comminfo[name] = comminfo def getcash(self): @@ -116,8 +159,12 @@ def getvalue(self, datas=None): raise NotImplementedError def get_fundshares(self): - '''Returns the current number of shares in the fund-like mode''' - return 1.0 # the abstract mode has only 1 share + '''返回 fund-like 模式下当前 share 数量。 + + Returns: + float: 当前 fund shares。抽象模式只有 1 份。 + ''' + return 1.0 # 抽象模式只有 1 share fundshares = property(get_fundshares) @@ -127,14 +174,20 @@ def get_fundvalue(self): fundvalue = property(get_fundvalue) def set_fundmode(self, fundmode, fundstartval=None): - '''Set the actual fundmode (True or False) + '''设置实际 fundmode。 - If the argument fundstartval is not ``None``, it will used + Args: + fundmode (bool): 是否启用 fundmode。 + fundstartval: 可选初始 fund value。如果不是 ``None``,子类可使用它。 ''' - pass # do nothing, not all brokers can support this + pass # 不执行任何操作,不是所有 broker 都支持该功能 def get_fundmode(self): - '''Returns the actual fundmode (True or False)''' + '''返回实际 fundmode。 + + Returns: + bool: 当前是否启用 fundmode。 + ''' return False fundmode = property(get_fundmode, set_fundmode) diff --git a/backtrader/brokers/__init__.py b/backtrader/brokers/__init__.py index 0efd3ce67..3baf453e6 100644 --- a/backtrader/brokers/__init__.py +++ b/backtrader/brokers/__init__.py @@ -21,22 +21,21 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) -# The modules below should/must define __all__ with the objects wishes -# or prepend an "_" (underscore) to private classes/variables +# 下列模块应在 __all__ 中定义要导出的对象,私有类/变量则使用 "_" 前缀 from .bbroker import BackBroker, BrokerBack try: from .ibbroker import IBBroker except ImportError: - pass # The user may not have ibpy installed + pass # 用户可能未安装 ibpy try: from .vcbroker import VCBroker except ImportError: - pass # The user may not have something installed + pass # 用户可能未安装相关模块 try: from .oandabroker import OandaBroker except ImportError as e: - pass # The user may not have something installed + pass # 用户可能未安装相关模块 diff --git a/backtrader/brokers/bbroker.py b/backtrader/brokers/bbroker.py index 1c7a64d80..1614a7550 100644 --- a/backtrader/brokers/bbroker.py +++ b/backtrader/brokers/bbroker.py @@ -34,191 +34,161 @@ class BackBroker(bt.BrokerBase): - '''Broker Simulator + '''Broker 模拟器。 - The simulation supports different order types, checking a submitted order - cash requirements against current cash, keeping track of cash and value - for each iteration of ``cerebro`` and keeping the current position on - different datas. + 该模拟器支持不同 order type,会用当前 cash 检查已提交 order 的资金需求, + 在 ``cerebro`` 每次迭代中跟踪 cash/value,并维护不同 data 上的当前 position。 - *cash* is adjusted on each iteration for instruments like ``futures`` for - which a price change implies in real brokers the addition/substracion of - cash. + 对 ``futures`` 这类 instrument,价格变化在真实 broker 中会增加/减少 cash, + 因此 *cash* 会在每次迭代中调整。 - Supported order types: + 支持的 order type: - - ``Market``: to be executed with the 1st tick of the next bar (namely - the ``open`` price) + - ``Market``: 使用下一根 bar 的第一个 tick(即 ``open`` price)执行。 - - ``Close``: meant for intraday in which the order is executed with the - closing price of the last bar of the session + - ``Close``: 面向 intraday,使用 session 最后一根 bar 的 close price 执行。 - - ``Limit``: executes if the given limit price is seen during the - session + - ``Limit``: 当 session 内出现给定 limit price 时执行。 - - ``Stop``: executes a ``Market`` order if the given stop price is seen + - ``Stop``: 当出现给定 stop price 时执行 ``Market`` order。 - - ``StopLimit``: sets a ``Limit`` order in motion if the given stop - price is seen + - ``StopLimit``: 当出现给定 stop price 时触发 ``Limit`` order。 - Because the broker is instantiated by ``Cerebro`` and there should be - (mostly) no reason to replace the broker, the params are not controlled - by the user for the instance. To change this there are two options: + 因为 broker 由 ``Cerebro`` 实例化,通常没有必要替换 broker 实例,所以参数不是 + 直接由用户控制。需要修改时有两种方式: - 1. Manually create an instance of this class with the desired params - and use ``cerebro.broker = instance`` to set the instance as the - broker for the ``run`` execution + 1. 用期望参数手动创建该类实例,并使用 ``cerebro.broker = instance`` 将其设为 + ``run`` 执行使用的 broker。 - 2. Use the ``set_xxx`` to set the value using - ``cerebro.broker.set_xxx`` where ```xxx`` stands for the name of the - parameter to set + 2. 使用 ``set_xxx`` 设置值,例如 ``cerebro.broker.set_xxx``,其中 ``xxx`` + 是要设置的参数名。 .. note:: - ``cerebro.broker`` is a *property* supported by the ``getbroker`` - and ``setbroker`` methods of ``Cerebro`` + ``cerebro.broker`` 是由 ``Cerebro`` 的 ``getbroker`` 和 ``setbroker`` + 方法支持的 *property*。 - Params: + Args: - - ``cash`` (default: ``10000``): starting cash + - ``cash``: 起始 cash。 - - ``commission`` (default: ``CommInfoBase(percabs=True)``) - base commission scheme which applies to all assets + - ``commission``: 适用于所有资产的基础 commission scheme。 - - ``checksubmit`` (default: ``True``) - check margin/cash before accepting an order into the system + - ``checksubmit``: 在接受 order 进入系统前检查 margin/cash。 - - ``eosbar`` (default: ``False``): - With intraday bars consider a bar with the same ``time`` as the end - of session to be the end of the session. This is not usually the - case, because some bars (final auction) are produced by many - exchanges for many products for a couple of minutes after the end of - the session + - ``eosbar``: 对 intraday bar,将 ``time`` 等于 session end 的 bar 视为 + session end。很多交易所会在 session end 后几分钟生成 final auction bar, + 因此默认不启用。 - ``filler`` (default: ``None``) - A callable with signature: ``callable(order, price, ago)`` + 一个签名为 ``callable(order, price, ago)`` 的 callable。 - - ``order``: obviously the order in execution. This provides access - to the *data* (and with it the *ohlc* and *volume* values), the - *execution type*, remaining size (``order.executed.remsize``) and - others. + - ``order``: 正在执行的 order。可通过它访问 *data*(以及其中的 + *ohlc* 和 *volume* 值)、*execution type*、剩余 size + (``order.executed.remsize``)等信息。 - Please check the ``Order`` documentation and reference for things - available inside an ``Order`` instance + ``Order`` 实例中可用的属性和方法请参考 ``Order`` 文档。 - - ``price`` the price at which the order is going to be executed in - the ``ago`` bar + - ``price``: order 将在 ``ago`` 对应 bar 上执行的 price。 - - ``ago``: index meant to be used with ``order.data`` for the - extraction of the *ohlc* and *volume* prices. In most cases this - will be ``0`` but on a corner case for ``Close`` orders, this - will be ``-1``. + - ``ago``: 与 ``order.data`` 配合使用的索引,用于提取 *ohlc* 和 + *volume*。多数情况下为 ``0``;``Close`` order 的一个边界场景下为 + ``-1``。 - In order to get the bar volume (for example) do: ``volume = - order.data.voluume[ago]`` + 例如获取 bar volume 可写为:``volume = order.data.volume[ago]``。 - The callable must return the *executed size* (a value >= 0) + callable 必须返回 *executed size*(值需 >= 0)。 - The callable may of course be an object with ``__call__`` matching - the aforementioned signature + callable 也可以是实现了上述 ``__call__`` 签名的对象。 - With the default ``None`` orders will be completely executed in a - single shot + 默认 ``None`` 时,order 会一次性完整执行。 - - ``slip_perc`` (default: ``0.0``) Percentage in absolute termns (and - positive) that should be used to slip prices up/down for buy/sell - orders + - ``slip_perc`` (default: ``0.0``): 以绝对百分比表示的正数,用于对 + buy/sell order 的 price 向上/向下做 slippage。 - Note: + 注意: - ``0.01`` is ``1%`` - ``0.001`` is ``0.1%`` - - ``slip_fixed`` (default: ``0.0``) Percentage in units (and positive) - that should be used to slip prices up/down for buy/sell orders + - ``slip_fixed`` (default: ``0.0``): 以价格单位表示的正数,用于对 + buy/sell order 的 price 向上/向下做 slippage。 - Note: if ``slip_perc`` is non zero, it takes precendence over this. + 注意:如果 ``slip_perc`` 非 0,它会优先于该参数。 - - ``slip_open`` (default: ``False``) whether to slip prices for order - execution which would specifically used the *opening* price of the - next bar. An example would be ``Market`` order which is executed with - the next available tick, i.e: the opening price of the bar. + - ``slip_open`` (default: ``False``): 对明确使用下一根 bar *opening* + price 执行的 order 是否应用 slippage。例如 ``Market`` order 会用下一 + 个可用 tick 执行,也就是该 bar 的 opening price。 - This also applies to some of the other executions, because the logic - tries to detect if the *opening* price would match the requested - price/execution type when moving to a new bar. + 这也适用于部分其它执行类型,因为进入新 bar 时逻辑会检测 *opening* + price 是否满足请求的 price/execution type。 - ``slip_match`` (default: ``True``) - If ``True`` the broker will offer a match by capping slippage at - ``high/low`` prices in case they would be exceeded. + 如果为 ``True``,当 slippage 超出 ``high/low`` 时,broker 会把 price + 限制在 ``high/low`` 内并提供 match。 - If ``False`` the broker will not match the order with the current - prices and will try execution during the next iteration + 如果为 ``False``,broker 不会用当前 price 匹配该 order,而会在下一轮 + 迭代中继续尝试执行。 - ``slip_limit`` (default: ``True``) - ``Limit`` orders, given the exact match price requested, will be - matched even if ``slip_match`` is ``False``. + ``Limit`` order 在请求了精确 match price 时,即使 ``slip_match`` 为 + ``False`` 也会被匹配。 - This option controls that behavior. + 该选项控制这一行为。 - If ``True``, then ``Limit`` orders will be matched by capping prices - to the ``limit`` / ``high/low`` prices + 如果为 ``True``,``Limit`` order 会把 price 限制在 ``limit`` / + ``high/low`` 后进行匹配。 - If ``False`` and slippage exceeds the cap, then there will be no - match + 如果为 ``False`` 且 slippage 超过限制,则不会 match。 - ``slip_out`` (default: ``False``) - Provide *slippage* even if the price falls outside the ``high`` - - ``low`` range. + 即使 price 落在 ``high`` - ``low`` 范围之外,也允许提供 *slippage*。 - ``coc`` (default: ``False``) - *Cheat-On-Close* Setting this to ``True`` with ``set_coc`` enables - matching a ``Market`` order to the closing price of the bar in which - the order was issued. This is actually *cheating*, because the bar - is *closed* and any order should first be matched against the prices - in the next bar + *Cheat-On-Close*。通过 ``set_coc`` 将其设为 ``True`` 后,``Market`` + order 可以匹配到发出 order 的同一根 bar 的 closing price。这实际上是 + *cheating*,因为该 bar 已经 *closed*,任何 order 按正常流程都应先在下一 + 根 bar 的 price 上尝试匹配。 - ``coo`` (default: ``False``) - *Cheat-On-Open* Setting this to ``True`` with ``set_coo`` enables - matching a ``Market`` order to the opening price, by for example - using a timer with ``cheat`` set to ``True``, because such a timer - gets executed before the broker has evaluated + *Cheat-On-Open*。通过 ``set_coo`` 将其设为 ``True`` 后,可以把 + ``Market`` order 匹配到 opening price。例如使用 ``cheat`` 为 ``True`` + 的 timer,因为这种 timer 会在 broker 评估前执行。 - ``int2pnl`` (default: ``True``) - Assign generated interest (if any) to the profit and loss of - operation that reduces a position (be it long or short). There may be - cases in which this is undesired, because different strategies are - competing and the interest would be assigned on a non-deterministic - basis to any of them. + 将产生的 interest(如果有)分配给减少 position 的操作(无论 long 还是 + short)的 profit/loss。某些场景下这并不理想,因为多个 strategy 可能竞争, + interest 会以非确定方式分配给其中任意一个。 - ``shortcash`` (default: ``True``) - If True then cash will be increased when a stocklike asset is shorted - and the calculated value for the asset will be negative. + 如果为 ``True``,做空 stocklike asset 时 cash 会增加,该 asset 的计算 + value 为负。 - If ``False`` then the cash will be deducted as operation cost and the - calculated value will be positive to end up with the same amount + 如果为 ``False``,cash 会作为操作成本扣除,计算 value 为正,最终总金额 + 保持一致。 - ``fundstartval`` (default: ``100.0``) - This parameter controls the start value for measuring the performance - in a fund-like way, i.e.: cash can be added and deducted increasing - the amount of shares. Performance is not measured using the net - asset value of the porftoflio but using the value of the fund + 该参数控制以类似 fund 的方式衡量 performance 时的起始 value。也就是说, + cash 可被增加或扣除,并相应改变份额数量。performance 不按 portfolio 的 + net asset value 衡量,而按 fund value 衡量。 - ``fundmode`` (default: ``False``) - If this is set to ``True`` analyzers like ``TimeReturn`` can - automatically calculate returns based on the fund value and not on - the total net asset value + 如果设为 ``True``,``TimeReturn`` 等 analyzer 可以基于 fund value 而不是 + total net asset value 自动计算 returns。 + + Returns: + BackBroker: 用于回测执行、资金核算和 order matching 的 broker。 ''' params = ( @@ -226,7 +196,7 @@ class BackBroker(bt.BrokerBase): ('checksubmit', True), ('eosbar', False), ('filler', None), - # slippage options + # slippage 选项 ('slip_perc', 0.0), ('slip_fixed', 0.0), ('slip_open', False), @@ -245,32 +215,32 @@ def __init__(self): super(BackBroker, self).__init__() self._userhist = [] self._fundhist = [] - # share_value, net asset value + # share_value, net asset value(净资产 value) self._fhistlast = [float('NaN'), float('NaN')] def init(self): super(BackBroker, self).init() self.startingcash = self.cash = self.p.cash self._value = self.cash - self._valuemkt = 0.0 # no open position + self._valuemkt = 0.0 # 无 open position - self._valuelever = 0.0 # no open position - self._valuemktlever = 0.0 # no open position + self._valuelever = 0.0 # 无 open position + self._valuemktlever = 0.0 # 无 open position - self._leverage = 1.0 # initially nothing is open - self._unrealized = 0.0 # no open position + self._leverage = 1.0 # 初始没有 open position + self._unrealized = 0.0 # 无 open position - self.orders = list() # will only be appending + self.orders = list() # 只会 append self.pending = collections.deque() # popleft and append(right) - self._toactivate = collections.deque() # to activate in next cycle + self._toactivate = collections.deque() # 下一轮需要 activate self.positions = collections.defaultdict(Position) - self.d_credit = collections.defaultdict(float) # credit per data + self.d_credit = collections.defaultdict(float) # 每个 data 的 credit self.notifs = collections.deque() self.submitted = collections.deque() - # to keep dependent orders if needed + # 按需保存依赖 order self._pchildren = collections.defaultdict(collections.deque) self._ocos = dict() @@ -289,44 +259,44 @@ def get_notification(self): return None def set_fundmode(self, fundmode, fundstartval=None): - '''Set the actual fundmode (True or False) + '''设置当前 fundmode(True 或 False)。 - If the argument fundstartval is not ``None``, it will used + 如果 ``fundstartval`` 不是 ``None``,会同步设置起始 fund value。 ''' self.p.fundmode = fundmode if fundstartval is not None: self.set_fundstartval(fundstartval) def get_fundmode(self): - '''Returns the actual fundmode (True or False)''' + '''返回当前 fundmode(True 或 False)。''' return self.p.fundmode fundmode = property(get_fundmode, set_fundmode) def set_fundstartval(self, fundstartval): - '''Set the starting value of the fund-like performance tracker''' + '''设置 fund-like performance tracker 的起始值。''' self.p.fundstartval = fundstartval def set_int2pnl(self, int2pnl): - '''Configure assignment of interest to profit and loss''' + '''配置是否将 interest 分配到 profit/loss。''' self.p.int2pnl = int2pnl def set_coc(self, coc): - '''Configure the Cheat-On-Close method to buy the close on order bar''' + '''配置 Cheat-On-Close,使 Market order 可按创建 bar 的 close 执行。''' self.p.coc = coc def set_coo(self, coo): - '''Configure the Cheat-On-Open method to buy the close on order bar''' + '''配置 Cheat-On-Open,使 Market order 可按 open 执行。''' self.p.coo = coo def set_shortcash(self, shortcash): - '''Configure the shortcash parameters''' + '''配置 shortcash 参数。''' self.p.shortcash = shortcash def set_slippage_perc(self, perc, slip_open=True, slip_limit=True, slip_match=True, slip_out=False): - '''Configure slippage to be percentage based''' + '''配置基于百分比的 slippage。''' self.p.slip_perc = perc self.p.slip_fixed = 0.0 self.p.slip_open = slip_open @@ -337,7 +307,7 @@ def set_slippage_perc(self, perc, def set_slippage_fixed(self, fixed, slip_open=True, slip_limit=True, slip_match=True, slip_out=False): - '''Configure slippage to be fixed points based''' + '''配置基于固定点数的 slippage。''' self.p.slip_perc = 0.0 self.p.slip_fixed = fixed self.p.slip_open = slip_open @@ -346,44 +316,44 @@ def set_slippage_fixed(self, fixed, self.p.slip_out = slip_out def set_filler(self, filler): - '''Sets a volume filler for volume filling execution''' + '''设置用于 volume filling execution 的 volume filler。''' self.p.filler = filler def set_checksubmit(self, checksubmit): - '''Sets the checksubmit parameter''' + '''设置 checksubmit 参数。''' self.p.checksubmit = checksubmit def set_eosbar(self, eosbar): - '''Sets the eosbar parameter (alias: ``seteosbar``''' + '''设置 eosbar 参数(别名:``seteosbar``)。''' self.p.eosbar = eosbar seteosbar = set_eosbar def get_cash(self): - '''Returns the current cash (alias: ``getcash``)''' + '''返回当前 cash(别名:``getcash``)。''' return self.cash getcash = get_cash def set_cash(self, cash): - '''Sets the cash parameter (alias: ``setcash``)''' + '''设置 cash 参数(别名:``setcash``)。''' self.startingcash = self.cash = self.p.cash = cash self._value = cash setcash = set_cash def add_cash(self, cash): - '''Add/Remove cash to the system (use a negative value to remove)''' + '''向系统增加/移除 cash(使用负值移除)。''' self._cash_addition.append(cash) def get_fundshares(self): - '''Returns the current number of shares in the fund-like mode''' + '''返回 fund-like 模式下当前 share 数量。''' return self._fundshares fundshares = property(get_fundshares) def get_fundvalue(self): - '''Returns the Fund-like share value''' + '''返回 fund-like share value。''' return self._fundval fundvalue = property(get_fundvalue) @@ -392,7 +362,7 @@ def cancel(self, order, bracket=False): try: self.pending.remove(order) except ValueError: - # If the list didn't have the element we didn't cancel anything + # 如果列表中没有该元素,则没有取消任何内容 return False order.cancel() @@ -403,8 +373,9 @@ def cancel(self, order, bracket=False): return True def get_value(self, datas=None, mkt=False, lever=False): - '''Returns the portfolio value of the given datas (if datas is ``None``, then - the total portfolio value will be returned (alias: ``getvalue``) + '''返回给定 data 的 portfolio value。 + + 如果 ``datas`` 为 ``None``,返回总 portfolio value(别名:``getvalue``)。 ''' if datas is None: if mkt: @@ -432,7 +403,7 @@ def _get_value(self, datas=None, lever=False): for data in datas or self.positions: comminfo = self.getcommissioninfo(data) position = self.positions[data] - # use valuesize: returns raw value, rather than negative adj val + # 使用 valuesize:返回原始 value,而不是负的调整 value if not self.p.shortcash: dvalue = comminfo.getvalue(position, data.close[0]) else: @@ -444,10 +415,10 @@ def _get_value(self, datas=None, lever=False): if lever and dvalue > 0: dvalue -= dunrealized return (dvalue / comminfo.get_leverage()) + dunrealized - return dvalue # raw data value requested, short selling is neg + return dvalue # 请求原始 data value,short selling 为负 if not self.p.shortcash: - dvalue = abs(dvalue) # short selling adds value in this case + dvalue = abs(dvalue) # 此时 short selling 会增加 value pos_value += dvalue unrealized += dunrealized @@ -463,7 +434,7 @@ def _get_value(self, datas=None, lever=False): self._value = v = self.cash + pos_value_unlever self._fundval = self._value / self._fundshares # update fundvalue else: - # Try to fetch a value + # 尝试获取 value fval, fvalue = self._process_fund_history() self._value = fvalue @@ -472,7 +443,7 @@ def _get_value(self, datas=None, lever=False): self._fundshares = fvalue / fval lev = pos_value / (pos_value_unlever or 1.0) - # update the calculated values above to the historical values + # 将上面计算出的值更新为历史值 pos_value_unlever = fvalue pos_value = fvalue * lev @@ -490,12 +461,11 @@ def get_leverage(self): return self._leverage def get_orders_open(self, safe=False): - '''Returns an iterable with the orders which are still open (either not - executed or partially executed + '''返回仍处于 open 状态的 order 可迭代对象。 - The orders returned must not be touched. + 包括尚未执行或部分执行的 order。返回的 order 不应被修改。 - If order manipulation is needed, set the parameter ``safe`` to True + 如需操作 order,请将 ``safe`` 参数设为 True。 ''' if safe: os = [x.clone() for x in self.pending] @@ -505,8 +475,7 @@ def get_orders_open(self, safe=False): return os def getposition(self, data): - '''Returns the current position status (a ``Position`` instance) for - the given ``data``''' + '''返回给定 ``data`` 的当前 position 状态(``Position`` 实例)。''' return self.positions[data] def orderstatus(self, order): @@ -523,8 +492,8 @@ def _take_children(self, order): if oref != pref: if pref not in self._pchildren: - order.reject() # parent not there - may have been rejected - self.notify(order) # reject child, notify + order.reject() # parent 不存在,可能已被 reject + self.notify(order) # reject 子 order 并通知 return None return pref @@ -535,12 +504,12 @@ def submit(self, order, check=True): return order pc = self._pchildren[pref] - pc.append(order) # store in parent/children queue + pc.append(order) # 存入 parent/children queue - if order.transmit: # if single order, sent and queue cleared - # if parent-child, the parent will be sent, the other kept + if order.transmit: # 若为单 order,则发送并清空 queue + # 若为 parent-child,则发送 parent,其他保留 rets = [self.transmit(x, check=check) for x in pc] - return rets[-1] # last one is the one triggering transmission + return rets[-1] # 最后一个是触发 transmission 的 order return order @@ -570,7 +539,7 @@ def check_submitted(self): position = positions.setdefault( order.data, self.positions[order.data].clone()) - # pseudo-execute the order to get the remaining cash after exec + # 伪执行 order,以得到执行后的剩余 cash cash = self._execute(order, cash=cash, position=position) if cash >= 0.0: @@ -594,20 +563,20 @@ def _bracketize(self, order, cancel=False): pref = getattr(order.parent, 'ref', oref) parent = oref == pref - pc = self._pchildren[pref] # defdict - guaranteed - if cancel or not parent: # cancel left or child exec -> cancel other + pc = self._pchildren[pref] # defdict,保证存在 + if cancel or not parent: # 取消剩余 order,或子 order 执行后取消其他 order while pc: - self.cancel(pc.popleft(), bracket=True) # idempotent + self.cancel(pc.popleft(), bracket=True) # 幂等 - del self._pchildren[pref] # defdict guaranteed + del self._pchildren[pref] # defdict 保证存在 - else: # not cancel -> parent exec'd - pc.popleft() # remove parent - for o in pc: # activate childnre + else: # 非取消 -> parent 已执行 + pc.popleft() # 移除 parent + for o in pc: # activate children self._toactivate.append(o) def _ococheck(self, order): - # ocoref = self._ocos[order.ref] or order.ref # a parent or self + # ocoref = self._ocos[order.ref] or order.ref # parent 或自身 parentref = self._ocos[order.ref] ocoref = self._ocos.get(parentref, None) ocol = self._ocol.pop(ocoref, None) @@ -622,12 +591,12 @@ def _ococheck(self, order): def _ocoize(self, order, oco): oref = order.ref if oco is None: - self._ocos[oref] = oref # current order is parent - self._ocol[oref].append(oref) # create ocogroup + self._ocos[oref] = oref # 当前 order 是 parent + self._ocol[oref].append(oref) # 创建 ocogroup else: - ocoref = self._ocos[oco.ref] # ref to group leader - self._ocos[oref] = ocoref # ref to group leader - self._ocol[ocoref].append(oref) # add to group + ocoref = self._ocos[oco.ref] # 指向 group leader + self._ocos[oref] = ocoref # 指向 group leader + self._ocol[ocoref].append(oref) # 加入 group def add_order_history(self, orders, notify=True): oiter = iter(orders) @@ -635,10 +604,10 @@ def add_order_history(self, orders, notify=True): self._userhist.append([o, oiter, notify]) def set_fund_history(self, fund): - # iterable with the following pro item + # 可迭代对象,每项格式如下 # [datetime, share_value, net asset value] fiter = iter(fund) - f = list(next(fiter)) # must not be empty + f = list(next(fiter)) # 不能为空 self._fundhist = [f, fiter] # self._fhistlast = f[1:] @@ -686,40 +655,39 @@ def sell(self, owner, data, def _execute(self, order, ago=None, price=None, cash=None, position=None, dtcoc=None): - # ago = None is used a flag for pseudo execution + # ago = None 用作伪执行标记 if ago is not None and price is None: - return # no psuedo exec no price - no execution + return # 非伪执行且无 price,则不执行 if self.p.filler is None or ago is None: - # Order gets full size or pseudo-execution + # order 使用完整 size 或执行伪执行 size = order.executed.remsize else: - # Execution depends on volume filler + # execution 取决于 volume filler size = self.p.filler(order, price, ago) if not order.isbuy(): size = -size - # Get comminfo object for the data + # 获取 data 对应的 comminfo 对象 comminfo = self.getcommissioninfo(order.data) - # Check if something has to be compensated + # 检查是否需要 compensate if order.data._compensate is not None: data = order.data._compensate - cinfocomp = self.getcommissioninfo(data) # for actual commission + cinfocomp = self.getcommissioninfo(data) # 用于实际 commission else: data = order.data cinfocomp = comminfo - # Adjust position with operation size + # 用 operation size 调整 position if ago is not None: - # Real execution with date + # 带日期的真实执行 position = self.positions[data] pprice_orig = position.price psize, pprice, opened, closed = position.pseudoupdate(size, price) - # if part/all of a position has been closed, then there has been - # a profitandloss ... record it + # 如果部分/全部 position 已关闭,则产生 profitandloss,需要记录 pnl = comminfo.profitandloss(-closed, pprice_orig, price) cash = self.cash else: @@ -727,9 +695,8 @@ def _execute(self, order, ago=None, price=None, cash=None, position=None, if not self.p.coo: price = pprice_orig = order.created.price else: - # When doing cheat on open, the price to be considered for a - # market order is the opening price and not the default closing - # price with which the order was created + # 使用 cheat on open 时,Market order 应考虑 opening price, + # 而不是创建 order 时默认的 closing price if order.exectype == Order.Market: price = pprice_orig = order.data.open[0] else: @@ -737,9 +704,9 @@ def _execute(self, order, ago=None, price=None, cash=None, position=None, psize, pprice, opened, closed = position.update(size, price) - # "Closing" totally or partially is possible. Cash may be re-injected + # 可全部或部分 "Closing",cash 可能被重新注入 if closed: - # Adjust to returned value for closed items & acquired opened items + # 按 closed item 返回值与 acquired opened item 调整 if self.p.shortcash: closedvalue = comminfo.getvaluesize(-closed, pprice_orig) else: @@ -747,21 +714,21 @@ def _execute(self, order, ago=None, price=None, cash=None, position=None, closecash = closedvalue if closedvalue > 0: # long position closed - closecash /= comminfo.get_leverage() # inc cash with lever + closecash /= comminfo.get_leverage() # 按 leverage 增加 cash cash += closecash + pnl * comminfo.stocklike - # Calculate and substract commission + # 计算并扣减 commission closedcomm = comminfo.getcommission(closed, price) cash -= closedcomm if ago is not None: - # Cashadjust closed contracts: prev close vs exec price - # The operation can inject or take cash out + # cashadjust closed contracts:prev close vs exec price。 + # 该操作可注入或取出 cash。 cash += comminfo.cashadjust(-closed, position.adjbase, price) - # Update system cash + # 更新系统 cash self.cash = cash else: closedvalue = closedcomm = 0.0 @@ -774,8 +741,8 @@ def _execute(self, order, ago=None, price=None, cash=None, position=None, openedvalue = comminfo.getoperationcost(opened, price) opencash = openedvalue - if openedvalue > 0: # long position being opened - opencash /= comminfo.get_leverage() # dec cash with level + if openedvalue > 0: # 正在打开 long position + opencash /= comminfo.get_leverage() # 按 leverage 减少 cash cash -= opencash # original behavior @@ -783,48 +750,44 @@ def _execute(self, order, ago=None, price=None, cash=None, position=None, cash -= openedcomm if cash < 0.0: - # execution is not possible - nullify + # cash 不足,无法执行,置空 opened = 0 openedvalue = openedcomm = 0.0 elif ago is not None: # real execution if abs(psize) > abs(opened): - # some futures were opened - adjust the cash of the - # previously existing futures to the operation price and - # use that as new adjustment base, because it already is - # for the new futures At the end of the cycle the - # adjustment to the close price will be done for all open - # futures from a common base price with regards to the - # close price + # 打开了部分 futures:将既有 futures 的 cash 调整到 operation price, + # 并将其作为新的 adjustment base。新 futures 已经使用该 base。 + # 周期末会基于共同 base price,对所有 open futures 按 close price 调整。 adjsize = psize - opened cash += comminfo.cashadjust(adjsize, position.adjbase, price) - # record adjust price base for end of bar cash adjustment + # 记录调整价格基准,用于 bar 末 cash adjustment position.adjbase = price - # update system cash - checking if opened is still != 0 + # 更新系统 cash,前提是 opened 仍不为 0 self.cash = cash else: openedvalue = openedcomm = 0.0 if ago is None: - # return cash from pseudo-execution + # 返回伪执行后的 cash return cash execsize = closed + opened if execsize: - # Confimrm the operation to the comminfo object + # 向 comminfo 对象确认该操作 comminfo.confirmexec(execsize, price) - # do a real position update if something was executed + # 如有实际执行,执行真实 position update position.update(execsize, price, data.datetime.datetime()) if closed and self.p.int2pnl: # Assign accumulated interest data closedcomm += self.d_credit.pop(data, 0.0) - # Execute and notify the order + # 执行并通知 order order.execute(dtcoc or data.datetime[ago], execsize, price, closed, closedvalue, closedcomm, @@ -838,7 +801,7 @@ def _execute(self, order, ago=None, price=None, cash=None, position=None, self._ococheck(order) if popened and not opened: - # opened was not executed - not enough cash + # opened 未执行,cash 不足 order.margin() self.notify(order) self._ococheck(order) @@ -870,18 +833,16 @@ def _try_exec_market(self, order, popen, phigh, plow): self._execute(order, ago=0, price=p, dtcoc=dtcoc) def _try_exec_close(self, order, pclose): - # pannotated allows to keep track of the closing bar if there is no - # information which lets us know that the current bar is the closing - # bar (like matching end of session bar) - # The actual matching will be done one bar afterwards but using the - # information from the actual closing bar + # 如果缺少信息判断当前 bar 是否为 closing bar(例如匹配 session end bar), + # pannotated 可用于跟踪 closing bar。 + # 实际 matching 会在下一根 bar 进行,但使用实际 closing bar 的信息。 dt0 = order.data.datetime[0] - # don't use "len" -> in replay the close can be reached with same len + # 不使用 "len":replay 中 close 可能在相同 len 下到达 if dt0 > order.created.dt: # can only execute after creation time # or (self.p.eosbar and dt0 == order.dteos): if dt0 >= order.dteos: - # past the end of session or right at it and eosbar is True + # 已超过 session end,或正好处于 session end 且 eosbar 为 True if order.pannotated and dt0 > order.dteos: ago = -1 execprice = order.pannotated @@ -892,54 +853,54 @@ def _try_exec_close(self, order, pclose): self._execute(order, ago=ago, price=execprice) return - # If no exexcution has taken place ... annotate the closing price + # 如果未发生 execution,记录 closing price order.pannotated = pclose def _try_exec_limit(self, order, popen, phigh, plow, plimit): if order.isbuy(): if plimit >= popen: - # open smaller/equal than requested - buy cheaper + # open 小于/等于请求价,以更便宜价格买入 pmax = min(phigh, plimit) p = self._slip_up(pmax, popen, doslip=self.p.slip_open, lim=True) self._execute(order, ago=0, price=p) elif plimit >= plow: - # day low below req price ... match limit price + # 日内 low 低于请求价,匹配 limit price self._execute(order, ago=0, price=plimit) else: # Sell if plimit <= popen: - # open greater/equal than requested - sell more expensive + # open 大于/等于请求价,以更高价格卖出 pmin = max(plow, plimit) p = self._slip_down(plimit, popen, doslip=self.p.slip_open, lim=True) self._execute(order, ago=0, price=p) elif plimit <= phigh: - # day high above req price ... match limit price + # 日内 high 高于请求价,匹配 limit price self._execute(order, ago=0, price=plimit) def _try_exec_stop(self, order, popen, phigh, plow, pcreated, pclose): if order.isbuy(): if popen >= pcreated: - # price penetrated with an open gap - use open + # price 通过开盘跳空穿透,使用 open p = self._slip_up(phigh, popen, doslip=self.p.slip_open) self._execute(order, ago=0, price=p) elif phigh >= pcreated: - # price penetrated during the session - use trigger price + # price 在 session 中穿透,使用 trigger price p = self._slip_up(phigh, pcreated) self._execute(order, ago=0, price=p) else: # Sell if popen <= pcreated: - # price penetrated with an open gap - use open + # price 通过开盘跳空穿透,使用 open p = self._slip_down(plow, popen, doslip=self.p.slip_open) self._execute(order, ago=0, price=p) elif plow <= pcreated: - # price penetrated during the session - use trigger price + # price 在 session 中穿透,使用 trigger price p = self._slip_down(plow, pcreated) self._execute(order, ago=0, price=p) - # not (completely) executed and trailing stop + # 未完全执行且为 trailing stop if order.alive() and order.exectype == Order.StopTrail: order.trailadjust(pclose) @@ -952,9 +913,9 @@ def _try_exec_stoplimit(self, order, self._try_exec_limit(order, popen, phigh, plow, plimit) elif phigh >= pcreated: - # price penetrated upwards during the session + # price 在 session 中向上穿透 order.triggered = True - # can calculate execution for a few cases - datetime is fixed + # 可为部分情况计算 execution;datetime 固定 if popen > pclose: if plimit >= pcreated: # limit above stop trigger p = self._slip_up(phigh, pcreated, lim=True) @@ -967,14 +928,14 @@ def _try_exec_stoplimit(self, order, self._execute(order, ago=0, price=p) else: # Sell if popen <= pcreated: - # price penetrated downwards with an open gap + # price 通过开盘跳空向下穿透 order.triggered = True self._try_exec_limit(order, popen, phigh, plow, plimit) elif plow <= pcreated: - # price penetrated downwards during the session + # price 在 session 中向下穿透 order.triggered = True - # can calculate execution for a few cases - datetime is fixed + # 可为部分情况计算 execution;datetime 固定 if popen <= pclose: if plimit <= pcreated: p = self._slip_down(plow, pcreated, lim=True) @@ -987,7 +948,7 @@ def _try_exec_stoplimit(self, order, p = self._slip_down(plow, pcreated, lim=True) self._execute(order, ago=0, price=p) - # not (completely) executed and trailing stop + # 未完全执行且为 trailing stop if order.alive() and order.exectype == Order.StopTrailLimit: order.trailadjust(pclose) @@ -1004,15 +965,15 @@ def _slip_up(self, pmax, price, doslip=True, lim=False): else: return price - if pslip <= pmax: # slipping can return price + if pslip <= pmax: # slippage 可返回 price return pslip elif self.p.slip_match or (lim and self.p.slip_limit): if not self.p.slip_out: return pmax - return pslip # non existent price + return pslip # 不存在于 bar 范围内的 price - return None # no price can be returned + return None # 无可返回 price def _slip_down(self, pmin, price, doslip=True, lim=False): if not doslip: @@ -1027,15 +988,15 @@ def _slip_down(self, pmin, price, doslip=True, lim=False): else: return price - if pslip >= pmin: # slipping can return price + if pslip >= pmin: # slippage 可返回 price return pslip elif self.p.slip_match or (lim and self.p.slip_limit): if not self.p.slip_out: return pmin - return pslip # non existent price + return pslip # 不存在于 bar 范围内的 price - return None # no price can be returned + return None # 无可返回 price def _try_exec(self, order): data = order.data @@ -1094,17 +1055,16 @@ def _process_fund_history(self): if '.' in dt: dtfmt += '.%f' dt = datetime.datetime.strptime(dt, dtfmt) - f[0] = dt # update value + f[0] = dt # 更新 value elif isinstance(dt, datetime.datetime): pass elif isinstance(dt, datetime.date): dt = datetime.datetime(year=dt.year, month=dt.month, day=dt.day) - f[0] = dt # Update the value + f[0] = dt # 更新 value - # Synchronization with the strategy is not possible because the broker - # is called before the strategy advances. The 2 lines below would do it - # if possible + # 无法与 strategy 同步,因为 broker 在 strategy 推进前被调用。 + # 如果可行,下面两行可完成同步。 # st0 = self.cerebro.runningstrats[0] # if dt <= st0.datetime.datetime(): if dt <= self.cerebro._dtmaster: @@ -1119,21 +1079,21 @@ def _process_order_history(self): while uhorder is not None: uhorder = list(uhorder) # to support assignment (if tuple) try: - dataidx = uhorder[3] # 2nd field + dataidx = uhorder[3] # 第 2 个字段 except IndexError: - dataidx = None # Field not present, use default + dataidx = None # 字段不存在,使用默认值 if dataidx is None: d = self.cerebro.datas[0] elif isinstance(dataidx, integer_types): d = self.cerebro.datas[dataidx] - else: # assume string + else: # 假设为 string d = self.cerebro.datasbyname[dataidx] if not len(d): - break # may start later as oter data feeds + break # 可能会像其他 data feed 一样稍后开始 - dt = uhorder[0] # date/datetime instance + dt = uhorder[0] # date/datetime 实例 if isinstance(dt, string_types): dtfmt = '%Y-%m-%d' if 'T' in dt: @@ -1151,7 +1111,7 @@ def _process_order_history(self): uhorder[0] = dt if dt > d.datetime.datetime(): - break # cannot execute yet 1st in queue, stop processing + break # queue 第 1 个尚不能执行,停止处理 size = uhorder[1] price = uhorder[2] @@ -1170,7 +1130,7 @@ def _process_order_history(self): histnotify=uhnotify, _checksubmit=False) - # update to next potential order + # 更新到下一个潜在 order uhist[0] = uhorder = next(uhorders, None) def next(self): @@ -1180,7 +1140,7 @@ def next(self): if self.p.checksubmit: self.check_submitted() - # Discount any cash for positions hold + # 扣除持仓产生的 cash credit = 0.0 for data, pos in self.positions.items(): if pos: @@ -1189,13 +1149,13 @@ def next(self): dcredit = comminfo.get_credit_interest(data, pos, dt0) self.d_credit[data] += dcredit credit += dcredit - pos.datetime = dt0 # mark last credit operation + pos.datetime = dt0 # 标记最后一次 credit 操作 self.cash -= credit self._process_order_history() - # Iterate once over all elements of the pending queue + # 遍历一次 pending queue 中的所有元素 self.pending.append(None) while True: order = self.pending.popleft() @@ -1208,7 +1168,7 @@ def next(self): self._bracketize(order, cancel=True) elif not order.active(): - self.pending.append(order) # cannot yet be processed + self.pending.append(order) # 尚不能处理 else: self._try_exec(order) @@ -1216,22 +1176,22 @@ def next(self): self.pending.append(order) elif order.status == Order.Completed: - # a bracket parent order may have been executed + # bracket parent order 可能已执行 self._bracketize(order) - # Operations have been executed ... adjust cash end of bar + # operation 已执行,bar 末调整 cash for data, pos in self.positions.items(): - # futures change cash every bar + # futures 每根 bar 都会改变 cash if pos: comminfo = self.getcommissioninfo(data) self.cash += comminfo.cashadjust(pos.size, pos.adjbase, data.close[0]) - # record the last adjustment price + # 记录最后调整价格 pos.adjbase = data.close[0] - self._get_value() # update value + self._get_value() # 更新 value -# Alias +# 别名 BrokerBack = BackBroker diff --git a/backtrader/brokers/ibbroker.py b/backtrader/brokers/ibbroker.py index 20e63dffc..5c82a2e9d 100644 --- a/backtrader/brokers/ibbroker.py +++ b/backtrader/brokers/ibbroker.py @@ -41,11 +41,11 @@ from backtrader.utils import AutoDict, AutoOrderedDict from backtrader.comminfo import CommInfoBase -bytes = bstr # py2/3 need for ibpy +bytes = bstr # ibpy 需要 py2/3 兼容 bytes class IBOrderState(object): - # wraps OrderState object and can print it + # 包装 OrderState 对象,并提供可打印表示 _fields = ['status', 'initMargin', 'maintMargin', 'equityWithLoan', 'commission', 'minCommission', 'maxCommission', 'commissionCurrency', 'warningText'] @@ -66,33 +66,26 @@ def __str__(self): class IBOrder(OrderBase, ib.ext.Order.Order): - '''Subclasses the IBPy order to provide the minimum extra functionality - needed to be compatible with the internally defined orders + '''IBPy order 的子类,用于提供与内部 order 兼容所需的最小扩展功能。 - Once ``OrderBase`` has processed the parameters, the __init__ method takes - over to use the parameter values and set the appropriate values in the - ib.ext.Order.Order object + ``OrderBase`` 处理参数后,``__init__`` 会接管这些参数值,并设置 + ``ib.ext.Order.Order`` 对象中的对应字段。 - Any extra parameters supplied with kwargs are applied directly to the - ib.ext.Order.Order object, which could be used as follows:: + kwargs 中提供的额外参数会直接应用到 ``ib.ext.Order.Order`` 对象,可按如下方式使用:: - Example: if the 4 order execution types directly supported by - ``backtrader`` are not enough, in the case of for example - *Interactive Brokers* the following could be passed as *kwargs*:: + 例如:如果 ``backtrader`` 直接支持的 order execution type 不够, + 对 *Interactive Brokers* 可通过 *kwargs* 传入:: orderType='LIT', lmtPrice=10.0, auxPrice=9.8 - This would override the settings created by ``backtrader`` and - generate a ``LIMIT IF TOUCHED`` order with a *touched* price of 9.8 - and a *limit* price of 10.0. + 这会覆盖 ``backtrader`` 创建的设置,并生成一个 ``LIMIT IF TOUCHED`` order, + 其中 *touched* price 为 9.8,*limit* price 为 10.0。 - This would be done almost always from the ``Buy`` and ``Sell`` methods of - the ``Strategy`` subclass being used in ``Cerebro`` + 该用法通常通过 ``Cerebro`` 中所用 ``Strategy`` 子类的 ``Buy`` 与 ``Sell`` 方法完成。 ''' def __str__(self): - '''Get the printout from the base class and add some ib.Order specific - fields''' + '''获取基类打印内容,并追加部分 ib.Order 专属字段。''' basetxt = super(IBOrder, self).__str__() tojoin = [basetxt] tojoin.append('Ref: {}'.format(self.ref)) @@ -106,7 +99,7 @@ def __str__(self): tojoin.append('GoodTillDate: {}'.format(self.m_goodTillDate)) return '\n'.join(tojoin) - # Map backtrader order types to the ib specifics + # 将 backtrader order type 映射到 IB 专属类型 _IBOrdTypes = { None: bytes('MKT'), # default Order.Market: bytes('MKT'), @@ -120,24 +113,22 @@ def __str__(self): def __init__(self, action, **kwargs): - # Marker to indicate an openOrder has been seen with - # PendinCancel/Cancelled which is indication of an upcoming - # cancellation + # 标记 openOrder 中曾出现 PendingCancel/Cancelled,表示即将取消 self._willexpire = False self.ordtype = self.Buy if action == 'BUY' else self.Sell super(IBOrder, self).__init__() - ib.ext.Order.Order.__init__(self) # Invoke 2nd base class + ib.ext.Order.Order.__init__(self) # 调用第 2 个基类 - # Now fill in the specific IB parameters + # 填充 IB 专属参数 self.m_orderType = self._IBOrdTypes[self.exectype] self.m_permid = 0 - # 'B' or 'S' should be enough + # 'B' 或 'S' 应已足够 self.m_action = bytes(action) - # Set the prices + # 设置价格 self.m_lmtPrice = 0.0 self.m_auxPrice = 0.0 @@ -156,25 +147,25 @@ def __init__(self, action, **kwargs): if self.trailamount is not None: self.m_auxPrice = self.trailamount elif self.trailpercent is not None: - # value expected in % format ... multiply 100.0 + # 期望值为百分比格式,因此乘以 100.0 self.m_trailingPercent = self.trailpercent * 100.0 elif self.exectype == self.StopTrailLimit: self.m_trailStopPrice = self.m_lmtPrice = self.price - # The limit offset is set relative to the price difference in TWS + # limit offset 在 TWS 中相对价格差设置 self.m_lmtPrice = self.pricelimit if self.trailamount is not None: self.m_auxPrice = self.trailamount elif self.trailpercent is not None: - # value expected in % format ... multiply 100.0 + # 期望值为百分比格式,因此乘以 100.0 self.m_trailingPercent = self.trailpercent * 100.0 - self.m_totalQuantity = abs(self.size) # ib takes only positives + self.m_totalQuantity = abs(self.size) # IB 只接受正数 self.m_transmit = self.transmit if self.parent is not None: self.m_parentId = self.parent.m_orderId - # Time In Force: DAY, GTC, IOC, GTD + # 有效期类型(Time In Force):DAY, GTC, IOC, GTD if self.valid is None: tif = 'GTC' # Good til cancelled elif isinstance(self.valid, (datetime, date)): @@ -185,7 +176,7 @@ def __init__(self, action, **kwargs): tif = 'DAY' else: tif = 'GTD' # Good til date - valid = datetime.now() + self.valid # .now, using localtime + valid = datetime.now() + self.valid # .now,使用本地时间 self.m_goodTillDate = bytes(valid.strftime('%Y%m%d %H:%M:%S')) elif self.valid == 0: @@ -198,67 +189,60 @@ def __init__(self, action, **kwargs): self.m_tif = bytes(tif) # OCA - self.m_ocaType = 1 # Cancel all remaining orders with block + self.m_ocaType = 1 # 带 block 取消所有剩余 order - # pass any custom arguments to the order + # 将自定义参数传给 order for k in kwargs: setattr(self, (not hasattr(self, k)) * 'm_' + k, kwargs[k]) class IBCommInfo(CommInfoBase): ''' - Commissions are calculated by ib, but the trades calculations in the - ```Strategy`` rely on the order carrying a CommInfo object attached for the - calculation of the operation cost and value. + IB 会计算 commissions,但 ``Strategy`` 中的 trade 计算依赖 order 携带 CommInfo + 对象,以计算操作成本和价值。 - These are non-critical informations, but removing them from the trade could - break existing usage and it is better to provide a CommInfo objet which - enables those calculations even if with approvimate values. + 这些信息不是核心执行路径,但移除可能破坏既有用法,因此提供一个可近似完成计算的 + CommInfo 对象。 - The margin calculation is not a known in advance information with IB - (margin impact can be gotten from OrderState objects) and therefore it is - left as future exercise to get it''' + margin 不是预先已知的信息(margin impact 可从 OrderState 对象获得),因此这里 + 保留近似计算。 + ''' def getvaluesize(self, size, price): - # In real life the margin approaches the price + # 实盘中 margin 接近 price return abs(size) * price def getoperationcost(self, size, price): - '''Returns the needed amount of cash an operation would cost''' - # Same reasoning as above + '''返回一次操作需要占用的 cash 数量。''' + # 与上方逻辑相同 return abs(size) * price class MetaIBBroker(BrokerBase.__class__): def __init__(cls, name, bases, dct): - '''Class has already been created ... register''' - # Initialize the class + '''类已经创建完成,执行 broker 注册。''' + # 初始化类 super(MetaIBBroker, cls).__init__(name, bases, dct) ibstore.IBStore.BrokerCls = cls class IBBroker(with_metaclass(MetaIBBroker, BrokerBase)): - '''Broker implementation for Interactive Brokers. + '''Interactive Brokers 的 broker 实现。 - This class maps the orders/positions from Interactive Brokers to the - internal API of ``backtrader``. + 该类将 Interactive Brokers 的 order/position 映射到 ``backtrader`` 内部 API。 - Notes: + 注意: - - ``tradeid`` is not really supported, because the profit and loss are - taken directly from IB. Because (as expected) calculates it in FIFO - manner, the pnl is not accurate for the tradeid. + - ``tradeid`` 并未真正支持,因为 profit/loss 直接来自 IB。IB 按 FIFO 方式计算, + 因此 pnl 对 tradeid 不精确。 - Position - If there is an open position for an asset at the beginning of - operaitons or orders given by other means change a position, the trades - calculated in the ``Strategy`` in cerebro will not reflect the reality. + 如果操作开始时某资产已有 open position,或其他方式发出的 order 改变了 + position,``Cerebro`` 中 ``Strategy`` 计算的 trade 将无法反映真实情况。 - To avoid this, this broker would have to do its own position - management which would also allow tradeid with multiple ids (profit and - loss would also be calculated locally), but could be considered to be - defeating the purpose of working with a live broker + 要避免该问题,broker 需要自行管理 position,并本地计算多 tradeid 的 + profit/loss;但这会削弱使用 live broker 的意义。 ''' params = () @@ -270,12 +254,12 @@ def __init__(self, **kwargs): self.startingcash = self.cash = 0.0 self.startingvalue = self.value = 0.0 - self._lock_orders = threading.Lock() # control access - self.orderbyid = dict() # orders by order id - self.executions = dict() # notified executions + self._lock_orders = threading.Lock() # 控制访问 + self.orderbyid = dict() # 按 order id 保存 order + self.executions = dict() # 已通知 execution self.ordstatus = collections.defaultdict(dict) - self.notifs = queue.Queue() # holds orders which are notified - self.tonotify = collections.deque() # hold oids to be notified + self.notifs = queue.Queue() # 保存需要通知的 order + self.tonotify = collections.deque() # 保存待通知 oid def start(self): super(IBBroker, self).start() @@ -294,7 +278,7 @@ def stop(self): self.ib.stop() def getcash(self): - # This call cannot block if no answer is available from ib + # 如果 IB 暂无响应,此调用不能阻塞 self.cash = self.ib.get_acc_cash() return self.cash @@ -309,9 +293,9 @@ def cancel(self, order): try: o = self.orderbyid[order.m_orderId] except (ValueError, KeyError): - return # not found ... not cancellable + return # 未找到,不可取消 - if order.status == Order.Cancelled: # already cancelled + if order.status == Order.Cancelled: # 已经取消 return self.ib.cancelOrder(order.m_orderId) @@ -327,8 +311,8 @@ def orderstatus(self, order): def submit(self, order): order.submit(self) - # ocoize if needed - if order.oco is None: # Generate a UniqueId + # 按需设置 OCO + if order.oco is None: # 生成 UniqueId order.m_ocaGroup = bytes(uuid.uuid4()) else: order.m_ocaGroup = self.orderbyid[order.oco.m_orderId].m_ocaGroup @@ -402,76 +386,69 @@ def get_notification(self): return None def next(self): - self.notifs.put(None) # mark notificatino boundary + self.notifs.put(None) # 标记通知边界 - # Order statuses in msg + # msg 中的 order status (SUBMITTED, FILLED, CANCELLED, INACTIVE, PENDINGSUBMIT, PENDINGCANCEL, PRESUBMITTED) = ( 'Submitted', 'Filled', 'Cancelled', 'Inactive', 'PendingSubmit', 'PendingCancel', 'PreSubmitted',) def push_orderstatus(self, msg): - # Cancelled and Submitted with Filled = 0 can be pushed immediately + # Cancelled 以及 Filled = 0 的 Submitted 可立即推送 try: order = self.orderbyid[msg.orderId] except KeyError: - return # not found, it was not an order + return # 未找到,不是当前 order if msg.status == self.SUBMITTED and msg.filled == 0: - if order.status == order.Accepted: # duplicate detection + if order.status == order.Accepted: # 重复检测 return order.accept(self) self.notify(order) elif msg.status == self.CANCELLED: - # duplicate detection + # 重复检测 if order.status in [order.Cancelled, order.Expired]: return if order._willexpire: - # An openOrder has been seen with PendingCancel/Cancelled - # and this happens when an order expires + # openOrder 曾出现 PendingCancel/Cancelled,这通常发生于 order 过期 order.expire() else: - # Pure user cancellation happens without an openOrder + # 纯用户取消不会出现 openOrder order.cancel() self.notify(order) elif msg.status == self.PENDINGCANCEL: - # In theory this message should not be seen according to the docs, - # but other messages like PENDINGSUBMIT which are similarly - # described in the docs have been received in the demo - if order.status == order.Cancelled: # duplicate detection + # 按文档理论上不应看到该消息,但 demo 中收到过类似文档描述的 PENDINGSUBMIT + if order.status == order.Cancelled: # 重复检测 return - # We do nothing because the situation is handled with the 202 error - # code if no orderStatus with CANCELLED is seen + # 这里不处理;若未看到 CANCELLED orderStatus,会由 202 error code 处理 # order.cancel() # self.notify(order) elif msg.status == self.INACTIVE: - # This is a tricky one, because the instances seen have led to - # order rejection in the demo, but according to the docs there may - # be a number of reasons and it seems like it could be reactivated - if order.status == order.Rejected: # duplicate detection + # 该状态较复杂:demo 中观察到它会导致 order rejection;但文档说明原因很多, + # 且看起来也可能重新激活。 + if order.status == order.Rejected: # 重复检测 return order.reject(self) self.notify(order) elif msg.status in [self.SUBMITTED, self.FILLED]: - # These two are kept inside the order until execdetails and - # commission are all in place - commission is the last to come + # 这两个状态会暂存在 order 中,直到 execdetails 与 commission 都到位。 + # commission 通常最后到达。 self.ordstatus[msg.orderId][msg.filled] = msg elif msg.status in [self.PENDINGSUBMIT, self.PRESUBMITTED]: - # According to the docs, these statuses can only be set by the - # programmer but the demo account sent it back at random times with - # "filled" + # 文档说这些状态只能由程序员设置,但 demo account 曾随机带着 "filled" 返回它们 if msg.filled: self.ordstatus[msg.orderId][msg.filled] = msg - else: # Unknown status ... + else: # 未知状态 pass def push_execution(self, ex): @@ -488,10 +465,10 @@ def push_commissionreport(self, cr): pprice_orig = position.price size = ex.m_shares if ex.m_side[0] == 'B' else -ex.m_shares price = ex.m_price - # use pseudoupdate and let the updateportfolio do the real update? + # 是否应使用 pseudoupdate,并让 updateportfolio 做真实更新? psize, pprice, opened, closed = position.update(size, price) - # split commission between closed and opened + # 在 closed/opened 之间拆分 commission comm = cr.m_commission closedcomm = comm * closed / size openedcomm = comm - closedcomm @@ -500,18 +477,18 @@ def push_commissionreport(self, cr): closedvalue = comminfo.getoperationcost(closed, pprice_orig) openedvalue = comminfo.getoperationcost(opened, price) - # default in m_pnl is MAXFLOAT + # m_pnl 默认值为 MAXFLOAT pnl = cr.m_realizedPNL if closed else 0.0 - # The internal broker calc should yield the same result + # 内部 broker 计算应得到相同结果 # pnl = comminfo.profitandloss(-closed, pprice_orig, price) - # Use the actual time provided by the execution object - # The report from TWS is in actual local time, not the data's tz + # 使用 execution 对象提供的真实时间。 + # TWS 报告使用实际本地时间,而不是 data 的 timezone。 dt = date2num(datetime.strptime(ex.m_time, '%Y%m%d %H:%M:%S')) - # Need to simulate a margin, but it plays no role, because it is - # controlled by a real broker. Let's set the price of the item + # 需要模拟 margin,但实际由真实 broker 控制,因此这里不起决定作用。 + # 使用当前 item price 作为 margin。 margin = order.data.close[0] order.execute(dt, size, price, @@ -522,18 +499,16 @@ def push_commissionreport(self, cr): if ostatus.status == self.FILLED: order.completed() - self.ordstatus.pop(oid) # nothing left to be reported + self.ordstatus.pop(oid) # 没有剩余内容需要报告 else: order.partial() - if oid not in self.tonotify: # Lock needed + if oid not in self.tonotify: # 需要锁 self.tonotify.append(oid) def push_portupdate(self): - # If the IBStore receives a Portfolio update, then this method will be - # indicated. If the execution of an order is split in serveral lots, - # updatePortfolio messages will be intermixed, which is used as a - # signal to indicate that the strategy can be notified + # IBStore 收到 Portfolio update 时会调用该方法。如果一个 order 的 execution + # 被拆成多笔,updatePortfolio 消息会穿插到达;这里将其作为 strategy 可被通知的信号。 with self._lock_orders: while self.tonotify: oid = self.tonotify.popleft() @@ -545,7 +520,7 @@ def push_ordererror(self, msg): try: order = self.orderbyid[msg.id] except (KeyError, AttributeError): - return # no order or no id in error + return # error 中没有 order 或 id if msg.errorCode == 202: if not order.alive(): @@ -558,7 +533,7 @@ def push_ordererror(self, msg): order.reject() else: - order.reject() # default for all other cases + order.reject() # 其他情况默认 reject self.notify(order) @@ -567,9 +542,9 @@ def push_orderstate(self, msg): try: order = self.orderbyid[msg.orderId] except (KeyError, AttributeError): - return # no order or no id in error + return # error 中没有 order 或 id if msg.orderState.m_status in ['PendingCancel', 'Cancelled', 'Canceled']: - # This is most likely due to an expiration] + # 这很可能来自 expiration order._willexpire = True diff --git a/backtrader/brokers/oandabroker.py b/backtrader/brokers/oandabroker.py index c584904be..f4bacbed9 100644 --- a/backtrader/brokers/oandabroker.py +++ b/backtrader/brokers/oandabroker.py @@ -40,36 +40,42 @@ class OandaCommInfo(CommInfoBase): def getvaluesize(self, size, price): - # In real life the margin approaches the price + # 实盘中 margin 接近 price return abs(size) * price def getoperationcost(self, size, price): - '''Returns the needed amount of cash an operation would cost''' - # Same reasoning as above + '''返回一次操作需要占用的 cash 数量。''' + # 与上方逻辑相同 return abs(size) * price class MetaOandaBroker(BrokerBase.__class__): def __init__(cls, name, bases, dct): - '''Class has already been created ... register''' - # Initialize the class + '''类已经创建完成,执行 broker 注册。''' + # 初始化类 super(MetaOandaBroker, cls).__init__(name, bases, dct) oandastore.OandaStore.BrokerCls = cls class OandaBroker(with_metaclass(MetaOandaBroker, BrokerBase)): - '''Broker implementation for Oanda. + '''Oanda 的 broker 实现。 - This class maps the orders/positions from Oanda to the - internal API of ``backtrader``. + 该类将 Oanda 的 order/position 映射到 ``backtrader`` 内部 API。 - Params: + Args: + use_positions: 连接 broker provider 时,是否使用已有 position 初始化 broker。 + 设为 ``False`` 可忽略已有 position。 + commission: Oanda 使用的 commission info。 - - ``use_positions`` (default:``True``): When connecting to the broker - provider use the existing positions to kickstart the broker. + Returns: + OandaBroker: 用于连接 Oanda store 并处理 order/position 的 broker。 - Set to ``False`` during instantiation to disregard any existing - position + --- + 交互界面使用示范: + + >>> broker = OandaBroker(use_positions=False) + >>> broker.p.use_positions + False ''' params = ( ('use_positions', True), @@ -81,11 +87,11 @@ def __init__(self, **kwargs): self.o = oandastore.OandaStore(**kwargs) - self.orders = collections.OrderedDict() # orders by order id - self.notifs = collections.deque() # holds orders which are notified + self.orders = collections.OrderedDict() # 按 order id 保存 order + self.notifs = collections.deque() # 保存需要通知的 order - self.opending = collections.defaultdict(list) # pending transmission - self.brackets = dict() # confirmed brackets + self.opending = collections.defaultdict(list) # 待传输 order + self.brackets = dict() # 已确认 bracket self.startingcash = self.cash = 0.0 self.startingvalue = self.value = 0.0 @@ -147,7 +153,7 @@ def stop(self): self.o.stop() def getcash(self): - # This call cannot block if no answer is available from oanda + # 如果 Oanda 暂无响应,此调用不能阻塞 self.cash = cash = self.o.get_cash() return cash @@ -202,28 +208,28 @@ def _expire(self, oref): self._bracketize(order, cancel=True) def _bracketnotif(self, order): - pref = getattr(order.parent, 'ref', order.ref) # parent ref or self - br = self.brackets.get(pref, None) # to avoid recursion + pref = getattr(order.parent, 'ref', order.ref) # parent ref 或自身 + br = self.brackets.get(pref, None) # 避免递归 return br[-2:] if br is not None else [] def _bracketize(self, order, cancel=False): - pref = getattr(order.parent, 'ref', order.ref) # parent ref or self - br = self.brackets.pop(pref, None) # to avoid recursion + pref = getattr(order.parent, 'ref', order.ref) # parent ref 或自身 + br = self.brackets.pop(pref, None) # 避免递归 if br is None: return if not cancel: - if len(br) == 3: # all 3 orders in place, parent was filled - br = br[1:] # discard index 0, parent + if len(br) == 3: # 3 个 order 均已就位,parent 已成交 + br = br[1:] # 丢弃索引 0,即 parent for o in br: - o.activate() # simulate activate for children - self.brackets[pref] = br # not done - reinsert children + o.activate() # 模拟激活子 order + self.brackets[pref] = br # 尚未完成,重新放入子 order - elif len(br) == 2: # filling a children - oidx = br.index(order) # find index to filled (0 or 1) - self._cancel(br[1 - oidx].ref) # cancel remaining (1 - 0 -> 1) + elif len(br) == 2: # 成交的是子 order + oidx = br.index(order) # 找到已成交索引(0 或 1) + self._cancel(br[1 - oidx].ref) # 取消剩余 order(1 - 0 -> 1) else: - # Any cancellation cancel the others + # 任一取消都会取消其他关联 order for o in br: if o.alive(): self._cancel(o.ref) @@ -241,7 +247,7 @@ def _fill(self, oref, size, price, ttype, **kwargs): self.put_notification(msg, order, price, size) return - # [main, stopside, takeside], neg idx to array are -3, -2, -1 + # [main, stopside, takeside],负索引分别为 -3、-2、-1 if ttype == 'STOP_LOSS_FILLED': order = self.brackets[pref][-2] elif ttype == 'TAKE_PROFIT_FILLED': @@ -280,26 +286,26 @@ def _fill(self, oref, size, price, ttype, **kwargs): def _transmit(self, order): oref = order.ref - pref = getattr(order.parent, 'ref', oref) # parent ref or self + pref = getattr(order.parent, 'ref', oref) # parent ref 或自身 if order.transmit: - if oref != pref: # children order - # Put parent in orders dict, but add stopside and takeside - # to order creation. Return the takeside order, to have 3s - takeside = order # alias for clarity + if oref != pref: # 子 order + # 将 parent 放入 orders dict,同时将 stopside 和 takeside 加入 order 创建。 + # 返回 takeside order,确保调用方得到完整 3 个 order 结构。 + takeside = order # 为可读性设置别名 parent, stopside = self.opending.pop(pref) for o in parent, stopside, takeside: - self.orders[o.ref] = o # write them down + self.orders[o.ref] = o # 记录 order self.brackets[pref] = [parent, stopside, takeside] self.o.order_create(parent, stopside, takeside) - return takeside # parent was already returned + return takeside # parent 已经返回过 - else: # Parent order, which is not being transmitted + else: # 将要传输的 parent order self.orders[order.ref] = order return self.o.order_create(order) - # Not transmitting + # 暂不传输 self.opending[pref].append(order) return order @@ -339,7 +345,7 @@ def sell(self, owner, data, def cancel(self, order): o = self.orders[order.ref] - if order.status == Order.Cancelled: # already cancelled + if order.status == Order.Cancelled: # 已经取消 return return self.o.order_cancel(order) @@ -354,4 +360,4 @@ def get_notification(self): return self.notifs.popleft() def next(self): - self.notifs.append(None) # mark notification boundary + self.notifs.append(None) # 标记通知边界 diff --git a/backtrader/brokers/vcbroker.py b/backtrader/brokers/vcbroker.py index 4c98d94e5..35f0d6c81 100644 --- a/backtrader/brokers/vcbroker.py +++ b/backtrader/brokers/vcbroker.py @@ -37,91 +37,68 @@ class VCCommInfo(CommInfoBase): ''' - Commissions are calculated by ib, but the trades calculations in the - ```Strategy`` rely on the order carrying a CommInfo object attached for the - calculation of the operation cost and value. + VisualChart 不通过 Trader interface 提供 commission,但 ``Strategy`` 中的 + trade 计算依赖 order 携带 CommInfo 对象,以计算操作成本和价值。 - These are non-critical informations, but removing them from the trade could - break existing usage and it is better to provide a CommInfo objet which - enables those calculations even if with approvimate values. + 这些信息不是核心执行路径,但移除可能破坏既有用法,因此提供一个可近似完成计算的 + CommInfo 对象。 - The margin calculation is not a known in advance information with IB - (margin impact can be gotten from OrderState objects) and therefore it is - left as future exercise to get it''' + margin 不是预先已知的信息,因此这里保留近似计算。 + ''' def getvaluesize(self, size, price): - # In real life the margin approaches the price + # 实盘中 margin 接近 price return abs(size) * price def getoperationcost(self, size, price): - '''Returns the needed amount of cash an operation would cost''' - # Same reasoning as above + '''返回一次操作需要占用的 cash 数量。''' + # 与上方逻辑相同 return abs(size) * price class MetaVCBroker(BrokerBase.__class__): def __init__(cls, name, bases, dct): - '''Class has already been created ... register''' - # Initialize the class + '''类已经创建完成,执行 broker 注册。''' + # 初始化类 super(MetaVCBroker, cls).__init__(name, bases, dct) vcstore.VCStore.BrokerCls = cls class VCBroker(with_metaclass(MetaVCBroker, BrokerBase)): - '''Broker implementation for VisualChart. - - This class maps the orders/positions from VisualChart to the - internal API of ``backtrader``. - - Params: - - - ``account`` (default: None) + '''VisualChart 的 broker 实现。 - VisualChart supports several accounts simultaneously on the broker. If - the default ``None`` is in place the 1st account in the ComTrader - ``Accounts`` collection will be used. + 该类将 VisualChart 的 order/position 映射到 ``backtrader`` 内部 API。 - If an account name is provided, the ``Accounts`` collection will be - checked and used if present + Args: + account: VisualChart 可同时支持多个 broker account。默认 ``None`` 时使用 + ComTrader ``Accounts`` collection 中的第 1 个 account;如果传入 account + 名称,则在 collection 中查找并使用。 + commission: commission scheme。未传入时会自动生成一个近似对象。 - - ``commission`` (default: None) - - An object will be autogenerated if no commission-scheme is passed as - parameter - - See the notes below for further explanations - - Notes: + Returns: + VCBroker: 用于连接 VisualChart store 并处理 order/position 的 broker。 + 注意: - Position - VisualChart reports "OpenPositions" updates through the ComTrader - interface but only when the position has a "size". An update to - indicate a position has moved to ZERO is reported by the absence of - such position. This forces to keep accounting of the positions by - looking at the execution events, just like the simulation broker does + VisualChart 通过 ComTrader interface 报告 "OpenPositions" 更新,但只在 + position 有 size 时报告。position 回到 ZERO 时表现为该 position 缺失, + 因此需要像模拟 broker 一样通过执行事件维护 position accounting。 - Commission - The ComTrader interface of VisualChart does not report commissions and - as such the auto-generated CommissionInfo object cannot use - non-existent commissions to properly account for them. In order to - support commissions a ``commission`` parameter has to be passed with - the appropriate commission schemes. - - The documentation on Commission Schemes details how to do this + VisualChart 的 ComTrader interface 不报告 commissions。若需要精确 commission, + 需要通过 ``commission`` 参数传入合适的 commission scheme。 - Expiration Timing - The ComTrader interface (or is it the comtypes module?) discards - ``time`` information from ``datetime`` objects and expiration dates are - always full dates. + ComTrader interface(或 comtypes 模块)会丢弃 ``datetime`` 对象中的 + ``time`` 信息,因此 expiration date 总是完整日期。 - Expiration Reporting - At the moment no heuristic is in place to determine when a cancelled - order has been cancelled due to expiration. And therefore expired - orders are reported as cancelled. + 当前没有启发式逻辑判断 cancelled order 是否因 expiration 被取消,因此 expired + order 会按 cancelled 报告。 ''' params = ( ('account', None), @@ -133,23 +110,23 @@ def __init__(self, **kwargs): self.store = vcstore.VCStore(**kwargs) - # Account data + # Account 数据 self._acc_name = None self.startingcash = self.cash = 0.0 self.startingvalue = self.value = 0.0 # Position accounting - self._lock_pos = threading.Lock() # sync account updates - self.positions = collections.defaultdict(Position) # actual positions + self._lock_pos = threading.Lock() # 同步 account 更新 + self.positions = collections.defaultdict(Position) # 实际 position - # Order storage - self._lock_orders = threading.Lock() # control access - self.orderbyid = dict() # orders by order id + # Order 存储 + self._lock_orders = threading.Lock() # 控制访问 + self.orderbyid = dict() # 按 order id 保存 order - # Notifications + # 通知 self.notifs = collections.deque() - # Dictionaries of values for order mapping + # order 映射所需的取值字典 self._otypes = { Order.Market: self.store.vcctmod.OT_Market, Order.Close: self.store.vcctmod.OT_Market, @@ -188,20 +165,20 @@ def stop(self): self.store.stop() def getcash(self): - # This call cannot block if no answer is available from ib + # 如果 VisualChart 暂无响应,此调用不能阻塞 return self.cash def getvalue(self, datas=None): return self.value def get_notification(self): - return self.notifs.popleft() # at leat a None is present + return self.notifs.popleft() # 至少存在一个 None def notify(self, order): self.notifs.append(order.clone()) def next(self): - self.notifs.append(None) # mark notificatino boundary + self.notifs.append(None) # 标记通知边界 def getposition(self, data, clone=True): with self._lock_pos: @@ -281,7 +258,7 @@ def _makeorder(self, ordtype, owner, data, elif not self.valid: # DAY order.TimeRestriction = self._otrestriction[Order.T_Day] - # Support for custom user arguments + # 支持自定义用户参数 for k in kwargs: if hasattr(order, k): setattr(order, k, kwargs[k]) @@ -342,11 +319,10 @@ def sell(self, owner, data, return self.submit(order, vcorder) # - # COM Events implementation + # COM Events 实现 # def __call__(self, trader): - # Called to start the process, call in sub-thread. only the passed - # trader can be used in the thread + # 在子线程中调用以启动流程;该线程中只能使用传入的 trader self.trader = trader for acc in trader.Accounts: @@ -354,24 +330,23 @@ def __call__(self, trader): self.startingcash = self.cash = acc.Balance.Cash self.startingvalue = self.value = acc.Balance.NetWorth self._acc_name = acc.Account - break # found the account + break # 找到 account return self def OnChangedBalance(self, Account): if self._acc_name is None or self._acc_name != Account: - return # skip notifs for other accounts + return # 跳过其他 account 的通知 for acc in self.trader.Accounts: if acc.Account == Account: - # Update store values + # 更新 store 值 self.cash = acc.Balance.Cash self.value = acc.Balance.NetWorth break def OnModifiedOrder(self, Order): - # We are not expecting this: unless backtrader starts implementing - # modify order method + # 当前不预期该事件;除非 backtrader 开始实现 modify order 方法 pass def OnCancelledOrder(self, Order): @@ -379,7 +354,7 @@ def OnCancelledOrder(self, Order): try: border = self.orderbyid[Order.OrderId] except KeyError: - return # possibly external order + return # 可能是外部 order border.cancel() self.notify(border) @@ -395,14 +370,14 @@ def OnExecutedOrder(self, Order, partial): try: border = self.orderbyid[Order.OrderId] except KeyError: - return # possibly external order + return # 可能是外部 order price = Order.Price size = Order.Volume if border.issell(): size *= -1 - # Find position and do a real update - accounting happens here + # 找到 position 并进行真实更新,accounting 在这里发生 position = self.getposition(border.data, clone=False) pprice_orig = position.price psize, pprice, opened, closed = position.update(size, price) @@ -417,8 +392,8 @@ def OnExecutedOrder(self, Order, partial): pnl = comminfo.profitandloss(-closed, pprice_orig, price) margin = comminfo.getvaluesize(size, price) - # NOTE: No commission information available in the Trader interface - # CHECK: Use reported time instead of last data time? + # 注意:Trader interface 无 commission 信息 + # 待确认:是否使用报告时间,而不是最后一个 data 时间? border.execute(border.data.datetime[0], size, price, closed, closedvalue, closedcomm, @@ -434,29 +409,27 @@ def OnExecutedOrder(self, Order, partial): self.notify(border) def OnOrderInMarket(self, Order): - # Other is in ther market ... therefore "accepted" + # order 已进入市场,因此视为 accepted with self._lock_orders: try: border = self.orderbyid[Order.OrderId] except KeyError: - return # possibly external order + return # 可能是外部 order border.accept() self.notify(border) def OnNewOrderLocation(self, Order): - # Can be used for "submitted", but the status is set manually + # 可用于 "submitted",但当前 status 是手动设置的 pass def OnChangedOpenPositions(self, Account): - # This would be useful if it reported a position moving back to 0. In - # this case the report contains a no-position and this doesn't help in - # the accounting. That's why the accounting is delegated to the - # reception of order execution + # 如果该事件报告 position 回到 0 会很有用;但实际报告中缺少该 position, + # 对 accounting 无帮助。因此 accounting 委托给 order execution 接收逻辑。 pass def OnNewClosedOperations(self, Account): - # This call-back has not been seen + # 尚未观察到该 callback pass def OnServerShutDown(self): diff --git a/backtrader/btrun/btrun.py b/backtrader/btrun/btrun.py index f93727629..104b56748 100644 --- a/backtrader/btrun/btrun.py +++ b/backtrader/btrun/btrun.py @@ -46,17 +46,17 @@ try: DATAFORMATS['vcdata'] = bt.feeds.VCData except AttributeError: - pass # no comtypes available + pass # comtypes 不可用 try: DATAFORMATS['ibdata'] = bt.feeds.IBData, except AttributeError: - pass # no ibpy available + pass # ibpy 不可用 try: DATAFORMATS['oandadata'] = bt.feeds.OandaData, except AttributeError: - pass # no oandapy available + pass # oandapy 不可用 TIMEFRAMES = dict( @@ -71,6 +71,11 @@ def btrun(pargs=''): + '''执行 backtrader 命令行入口。 + + Args: + pargs: 可选命令行参数;默认空字符串时从 ``sys.argv`` 解析。 + ''' args = parse_args(pargs) if args.flush: @@ -91,13 +96,13 @@ def btrun(pargs=''): elif args.replay is not None: tfcp = args.replay.split(':') - # compression may be skipped and it will default to 1 + # compression 可省略,默认值为 1 if len(tfcp) == 1 or tfcp[1] == '': tf, cp = tfcp[0], 1 else: tf, cp = tfcp - cp = int(cp) # convert any value to int + cp = int(cp) # 将任意值转换为 int tf = TIMEFRAMES.get(tf, None) for data in getdatas(args): @@ -108,13 +113,13 @@ def btrun(pargs=''): else: cerebro.adddata(data) - # get and add signals + # 获取并添加 signals signals = getobjects(args.signals, bt.Indicator, bt.signals, issignal=True) for sig, kwargs, sigtype in signals: stype = getattr(bt.signal, 'SIGNAL_' + sigtype.upper()) cerebro.add_signal(stype, sig, **kwargs) - # get and add strategies + # 获取并添加 strategies strategies = getobjects(args.strategies, bt.Strategy, bt.strategies) for strat, kwargs in strategies: cerebro.addstrategy(strat, **kwargs) @@ -141,7 +146,7 @@ def btrun(pargs=''): for hook, kwargs in ans: hook(cerebro, **kwargs) runsts = cerebro.run() - runst = runsts[0] # single strategy and no optimization + runst = runsts[0] # 单 strategy 且无 optimization if args.pranalyzer or args.ppranalyzer: if runst.analyzers: @@ -160,7 +165,7 @@ def btrun(pargs=''): if args.plot: pkwargs = dict(style='bar') if args.plot is not True: - # evaluates to True but is not "True" - args were passed + # 表达式为 True 但不是 "True",说明传入了 args ekwargs = eval('dict(' + args.plot + ')') pkwargs.update(ekwargs) @@ -169,6 +174,12 @@ def btrun(pargs=''): def setbroker(args, cerebro): + '''根据 CLI 参数配置 broker。 + + Args: + args: ``parse_args`` 返回的参数对象。 + cerebro: 需要配置 broker 的 ``Cerebro`` 实例。 + ''' broker = cerebro.getbroker() if args.cash is not None: @@ -202,10 +213,12 @@ def setbroker(args, cerebro): def getdatas(args): - # Get the data feed class from the global dictionary + '''根据 CLI 参数创建 data feed 列表。''' + + # 从全局 dictionary 获取 data feed class dfcls = DATAFORMATS[args.format] - # Prepare some args + # 准备参数 dfkwargs = dict() if args.format == 'yahoo_unreversed': dfkwargs['reverse'] = True @@ -243,6 +256,16 @@ def getdatas(args): def getmodclasses(mod, clstype, clsname=None): + '''从 module 中获取指定类型的 class。 + + Args: + mod: 要扫描的 module。 + clstype: class 必须继承的基类。 + clsname: 可选 class 名称;未传入时返回所有匹配 class。 + + Returns: + list: 匹配到的 class 列表。 + ''' clsmembers = inspect.getmembers(mod, inspect.isclass) clslist = list() @@ -261,6 +284,15 @@ def getmodclasses(mod, clstype, clsname=None): def getmodfunctions(mod, funcname=None): + '''从 module 中获取 function/method。 + + Args: + mod: 要扫描的 module。 + funcname: 可选函数名;未传入时返回所有 function/method。 + + Returns: + list: 匹配到的 function/method 列表。 + ''' members = inspect.getmembers(mod, inspect.isfunction) + \ inspect.getmembers(mod, inspect.ismethod) @@ -277,7 +309,17 @@ def getmodfunctions(mod, funcname=None): def loadmodule(modpath, modname=''): - # generate a random name for the module + '''按路径加载 Python module。 + + Args: + modpath: module 文件路径,可省略 ``.py`` 后缀。 + modname: 可选 module 名称;未传入时自动生成随机名称。 + + Returns: + tuple: ``(module, error)``,加载成功时 error 为 ``None``。 + ''' + + # 为 module 生成随机名称 if not modpath.endswith('.py'): modpath += '.py' @@ -297,6 +339,7 @@ def loadmodule(modpath, modname=''): def loadmodule2(modpath, modname): + '''使用 Python 2 兼容方式加载 module。''' import imp try: @@ -308,6 +351,7 @@ def loadmodule2(modpath, modname): def loadmodule3(modpath, modname): + '''使用 Python 3 importlib loader 加载 module。''' import importlib.machinery try: @@ -320,6 +364,18 @@ def loadmodule3(modpath, modname): def getobjects(iterable, clsbase, modbase, issignal=False): + '''从 CLI 声明中解析并加载 class 对象。 + + Args: + iterable: CLI 中传入的对象声明列表。 + clsbase: 目标 class 必须继承的基类。 + modbase: 未指定 module 时使用的默认 module。 + issignal: 是否按 signal 语法解析声明。 + + Returns: + list: 普通对象返回 ``(class, kwargs)``;signal 返回 + ``(class, kwargs, sigtype)``。 + ''' retobjects = list() for item in iterable or []: @@ -340,7 +396,7 @@ def getobjects(iterable, clsbase, modbase, issignal=False): modpath, name = tokens kwtokens = name.split(':', 1) if len(kwtokens) == 1: - # no '(' found + # 未找到 '(' kwargs = dict() else: name = kwtokens[0] @@ -371,6 +427,15 @@ def getobjects(iterable, clsbase, modbase, issignal=False): return retobjects def getfunctions(iterable, modbase): + '''从 CLI 声明中解析并加载 function。 + + Args: + iterable: CLI 中传入的 function 声明列表。 + modbase: 未指定 module 时使用的默认 module。 + + Returns: + list: ``(function, kwargs)`` 元组列表。 + ''' retfunctions = list() for item in iterable or []: @@ -384,7 +449,7 @@ def getfunctions(iterable, modbase): modpath, name = tokens kwtokens = name.split(':', 1) if len(kwtokens) == 1: - # no '(' found + # 未找到 '(' kwargs = dict() else: name = kwtokens[0] @@ -413,13 +478,21 @@ def getfunctions(iterable, modbase): def parse_args(pargs=''): + '''解析命令行参数。 + + Args: + pargs: 可选参数列表;为空时使用默认命令行参数。 + + Returns: + argparse.Namespace: 解析后的参数对象。 + ''' parser = argparse.ArgumentParser( description='Backtrader Run Script', formatter_class=argparse.RawTextHelpFormatter, ) group = parser.add_argument_group(title='Data options') - # Data options + # Data options(命令行分组标题保持英文) group.add_argument('--data', '-d', action='append', required=True, help='Data files to be added to the system') @@ -510,7 +583,7 @@ def parse_args(pargs=''): 'cerebro, beyond options provided by this script\n\n') ) - # Module where to read the strategy from + # 读取 strategy 的 module group = parser.add_argument_group(title='Strategy options') group.add_argument( '--strategy', '-st', dest='strategies', @@ -537,7 +610,7 @@ def parse_args(pargs=''): ' - module or module::kwargs') ) - # Module where to read the strategy from + # 读取 signal 的 module group = parser.add_argument_group(title='Signals') group.add_argument( '--signal', '-sig', dest='signals', @@ -571,7 +644,7 @@ def parse_args(pargs=''): ' - module or module:::kwargs') ) - # Observers + # Observers(命令行分组标题保持英文) group = parser.add_argument_group(title='Observers and statistics') group.add_argument( '--observer', '-ob', dest='observers', @@ -597,7 +670,7 @@ def parse_args(pargs=''): '\n' ' - module or module::kwargs') ) - # Analyzers + # Analyzers(命令行分组标题保持英文) group = parser.add_argument_group(title='Analyzers') group.add_argument( '--analyzer', '-an', dest='analyzers', @@ -624,7 +697,7 @@ def parse_args(pargs=''): ' - module or module::kwargs') ) - # Analyzer - Print + # Analyzer - Print(命令行分组标题保持英文) group = parser.add_mutually_exclusive_group(required=False) group.add_argument('--pranalyzer', '-pralyzer', required=False, action='store_true', @@ -634,7 +707,7 @@ def parse_args(pargs=''): required=False, action='store_true', help=('Automatically PRETTY print analyzers')) - # Indicators + # Indicators(命令行分组标题保持英文) group = parser.add_argument_group(title='Indicators') group.add_argument( '--indicator', '-ind', dest='indicators', @@ -661,7 +734,7 @@ def parse_args(pargs=''): ' - module or module::kwargs') ) - # Writer + # Writer(命令行分组标题保持英文) group = parser.add_argument_group(title='Writers') group.add_argument( '--writer', '-wr', @@ -682,7 +755,7 @@ def parse_args(pargs=''): 'Please see the documentation for the available kwargs') ) - # Broker/Commissions + # Broker/Commissions(命令行分组标题保持英文) group = parser.add_argument_group(title='Cash and Commission Scheme Args') group.add_argument('--cash', '-cash', required=False, type=float, help='Cash to set to the broker') @@ -717,11 +790,11 @@ def parse_args(pargs=''): group.add_argument('--slip_out', required=False, action='store_true', help='with slip_match enabled, match outside high-low') - # Output flushing + # Output flushing(命令行选项保持英文) group.add_argument('--flush', required=False, action='store_true', help='flush the output - useful under win32 systems') - # Plot options + # Plot options(命令行分组标题保持英文) parser.add_argument( '--plot', '-p', nargs='?', metavar='kwargs', diff --git a/backtrader/cerebro.py b/backtrader/cerebro.py index 9b18e7775..35ec58352 100644 --- a/backtrader/cerebro.py +++ b/backtrader/cerebro.py @@ -47,10 +47,12 @@ PandasMarketCalendar) from .timer import Timer -# Defined here to make it pickable. Ideally it could be defined inside Cerebro +# 定义在这里以便 pickle。理想情况下它可以定义在 Cerebro 内部。 class OptReturn(object): + '''optimization 返回值容器,用于在 ``optreturn`` 模式下保存精简结果。''' + def __init__(self, params, **kwargs): self.p = self.params = params for k, v in kwargs.items(): @@ -58,216 +60,187 @@ def __init__(self, params, **kwargs): class Cerebro(with_metaclass(MetaParams, object)): - '''Params: + '''backtrader 的核心调度引擎。 + + 该类负责管理 data feeds、strategy、broker、observer、analyzer、writer、 + timer 以及回测/优化运行流程。 + + Args: - ``preload`` (default: ``True``) - Whether to preload the different ``data feeds`` passed to cerebro for - the Strategies + 是否为 strategy 预加载传入 cerebro 的不同 ``data feeds``。 - ``runonce`` (default: ``True``) - Run ``Indicators`` in vectorized mode to speed up the entire system. - Strategies and Observers will always be run on an event based basis + 是否以 vectorized mode 运行 ``Indicators``,以提升整体速度。 + Strategies 和 Observers 始终按 event based 方式运行。 - ``live`` (default: ``False``) - If no data has reported itself as *live* (via the data's ``islive`` - method but the end user still want to run in ``live`` mode, this - parameter can be set to true + 如果没有 data 通过 ``islive`` 报告自己是 *live*,但用户仍希望按 ``live`` 模式 + 运行,可将该参数设为 ``True``。 - This will simultaneously deactivate ``preload`` and ``runonce``. It - will have no effect on memory saving schemes. + 这会同时停用 ``preload`` 和 ``runonce``,但不会影响 memory saving scheme。 - Run ``Indicators`` in vectorized mode to speed up the entire system. - Strategies and Observers will always be run on an event based basis + ``Indicators`` 不再按 vectorized mode 运行,而是与 live feed 一样逐步推进。 - ``maxcpus`` (default: None -> all available cores) - How many cores to use simultaneously for optimization + optimization 时同时使用多少 CPU core。 - ``stdstats`` (default: ``True``) - If True default Observers will be added: Broker (Cash and Value), - Trades and BuySell + 如果为 ``True``,会自动添加默认 Observers:Broker(Cash 和 Value)、Trades + 以及 BuySell。 - ``oldbuysell`` (default: ``False``) - If ``stdstats`` is ``True`` and observers are getting automatically - added, this switch controls the main behavior of the ``BuySell`` - observer + 当 ``stdstats`` 为 ``True`` 且自动添加 observers 时,该开关控制 ``BuySell`` + observer 的主要行为。 - - ``False``: use the modern behavior in which the buy / sell signals - are plotted below / above the low / high prices respectively to avoid - cluttering the plot + - ``False``: 使用现代行为,buy/sell signal 分别绘制在 low/high price + 下方/上方,以避免图形拥挤。 - - ``True``: use the deprecated behavior in which the buy / sell signals - are plotted where the average price of the order executions for the - given moment in time is. This will of course be on top of an OHLC bar - or on a Line on Cloe bar, difficulting the recognition of the plot. + - ``True``: 使用旧行为,把 buy/sell signal 绘制在该时刻 order execution 的 + average price 上。这会覆盖 OHLC bar 或 Line on Close bar,使图形更难识别。 - ``oldtrades`` (default: ``False``) - If ``stdstats`` is ``True`` and observers are getting automatically - added, this switch controls the main behavior of the ``Trades`` - observer + 当 ``stdstats`` 为 ``True`` 且自动添加 observers 时,该开关控制 ``Trades`` + observer 的主要行为。 - - ``False``: use the modern behavior in which trades for all datas are - plotted with different markers + - ``False``: 使用现代行为,对所有 datas 的 trades 使用不同 marker 绘制。 - - ``True``: use the old Trades observer which plots the trades with the - same markers, differentiating only if they are positive or negative + - ``True``: 使用旧版 Trades observer,所有 trades 使用相同 marker,仅区分 + 正负。 - ``exactbars`` (default: ``False``) - With the default value each and every value stored in a line is kept in - memory + 默认值下,line 中保存的每个 value 都会保留在内存中。 - Possible values: - - ``True`` or ``1``: all "lines" objects reduce memory usage to the - automatically calculated minimum period. + 可选值: + - ``True`` or ``1``: 所有 "lines" 对象都会把内存使用量降到自动计算出的 + minimum period。 - If a Simple Moving Average has a period of 30, the underlying data - will have always a running buffer of 30 bars to allow the - calculation of the Simple Moving Average + 例如 Simple Moving Average 的 period 为 30,则底层 data 始终保留 30 根 + bar 的 running buffer,以便计算该 Moving Average。 - - This setting will deactivate ``preload`` and ``runonce`` - - Using this setting also deactivates **plotting** + - 该设置会停用 ``preload`` 和 ``runonce``。 + - 使用该设置也会停用 **plotting**。 - - ``-1``: datafreeds and indicators/operations at strategy level will - keep all data in memory. + - ``-1``: strategy 层级的 data feeds 和 indicators/operations 会在内存中 + 保留全部 data。 - For example: a ``RSI`` internally uses the indicator ``UpDay`` to - make calculations. This subindicator will not keep all data in - memory + 例如 ``RSI`` 内部使用 ``UpDay`` indicator 计算。这个 subindicator 不会在 + 内存中保留全部 data。 - - This allows to keep ``plotting`` and ``preloading`` active. + - 这允许保持 ``plotting`` 和 ``preloading`` 激活。 - - ``runonce`` will be deactivated + - ``runonce`` 会被停用。 - - ``-2``: data feeds and indicators kept as attributes of the - strategy will keep all points in memory. + - ``-2``: 作为 strategy 属性保存的 data feeds 和 indicators 会在内存中 + 保留全部 points。 - For example: a ``RSI`` internally uses the indicator ``UpDay`` to - make calculations. This subindicator will not keep all data in - memory + 例如 ``RSI`` 内部使用 ``UpDay`` indicator 计算。这个 subindicator 不会在 + 内存中保留全部 data。 - If in the ``__init__`` something like - ``a = self.data.close - self.data.high`` is defined, then ``a`` - will not keep all data in memory + 如果在 ``__init__`` 中定义了类似 ``a = self.data.close - self.data.high`` + 的表达式,则 ``a`` 不会在内存中保留全部 data。 - - This allows to keep ``plotting`` and ``preloading`` active. + - 这允许保持 ``plotting`` 和 ``preloading`` 激活。 - - ``runonce`` will be deactivated + - ``runonce`` 会被停用。 - ``objcache`` (default: ``False``) - Experimental option to implement a cache of lines objects and reduce - the amount of them. Example from UltimateOscillator:: + 实验性选项:为 lines object 实现 cache,以减少对象数量。例如 UltimateOscillator:: bp = self.data.close - TrueLow(self.data) - tr = TrueRange(self.data) # -> creates another TrueLow(self.data) + tr = TrueRange(self.data) # -> 创建另一个 TrueLow(self.data) - If this is ``True`` the 2nd ``TrueLow(self.data)`` inside ``TrueRange`` - matches the signature of the one in the ``bp`` calculation. It will be - reused. + 如果为 ``True``,``TrueRange`` 内部第 2 个 ``TrueLow(self.data)`` 与 ``bp`` + 计算中的对象签名匹配,则会被复用。 - Corner cases may happen in which this drives a line object off its - minimum period and breaks things and it is therefore disabled. + 某些边界场景可能导致 line object 偏离其 minimum period 并破坏计算,因此默认禁用。 - ``writer`` (default: ``False``) - If set to ``True`` a default WriterFile will be created which will - print to stdout. It will be added to the strategy (in addition to any - other writers added by the user code) + 如果设为 ``True``,会创建一个默认 ``WriterFile`` 并输出到 stdout。它会被添加 + 到 strategy 中,同时不影响用户代码添加的其它 writers。 - ``tradehistory`` (default: ``False``) - If set to ``True``, it will activate update event logging in each trade - for all strategies. This can also be accomplished on a per strategy - basis with the strategy method ``set_tradehistory`` + 如果设为 ``True``,会为所有 strategies 中的每个 trade 激活 update event + logging。也可以在单个 strategy 上通过 ``set_tradehistory`` 实现。 - ``optdatas`` (default: ``True``) - If ``True`` and optimizing (and the system can ``preload`` and use - ``runonce``, data preloading will be done only once in the main process - to save time and resources. + 如果为 ``True`` 且正在 optimizing,并且系统可以 ``preload`` 且使用 ``runonce``, + data preloading 只会在主进程中执行一次,以节省时间和资源。 - The tests show an approximate ``20%`` speed-up moving from a sample - execution in ``83`` seconds to ``66`` + 测试显示大约有 ``20%`` 提速,示例运行从 ``83`` 秒降到 ``66`` 秒。 - ``optreturn`` (default: ``True``) - If ``True`` the optimization results will not be full ``Strategy`` - objects (and all *datas*, *indicators*, *observers* ...) but and object - with the following attributes (same as in ``Strategy``): + 如果为 ``True``,optimization 结果不会返回完整 ``Strategy`` 对象(以及所有 + *datas*、*indicators*、*observers* ...),而是返回带以下属性的精简对象 + (与 ``Strategy`` 中同名): - - ``params`` (or ``p``) the strategy had for the execution - - ``analyzers`` the strategy has executed + - ``params``(或 ``p``):本次执行中的 strategy 参数。 + - ``analyzers``:strategy 执行过的 analyzers。 - In most occassions, only the *analyzers* and with which *params* are - the things needed to evaluate a the performance of a strategy. If - detailed analysis of the generated values for (for example) - *indicators* is needed, turn this off + 多数情况下,评估 strategy performance 只需要 *analyzers* 和对应 *params*。 + 如果需要详细分析生成的值,例如 *indicators*,请关闭该选项。 - The tests show a ``13% - 15%`` improvement in execution time. Combined - with ``optdatas`` the total gain increases to a total speed-up of - ``32%`` in an optimization run. + 测试显示执行时间提升 ``13% - 15%``。与 ``optdatas`` 组合时,optimization run + 的总提速可达 ``32%``。 - ``oldsync`` (default: ``False``) - Starting with release 1.9.0.99 the synchronization of multiple datas - (same or different timeframes) has been changed to allow datas of - different lengths. + 从 1.9.0.99 开始,多 data(相同或不同 timeframe)的同步机制已改变,以允许 + 不同长度的 datas。 - If the old behavior with data0 as the master of the system is wished, - set this parameter to true + 如果希望使用以 data0 作为系统 master 的旧行为,请将该参数设为 ``True``。 - ``tz`` (default: ``None``) - Adds a global timezone for strategies. The argument ``tz`` can be + 为 strategies 添加全局 timezone。``tz`` 可以是: - - ``None``: in this case the datetime displayed by strategies will be - in UTC, which has been always the standard behavior + - ``None``: strategy 显示的 datetime 为 UTC,这是一直以来的标准行为。 - - ``pytz`` instance. It will be used as such to convert UTC times to - the chosen timezone + - ``pytz`` instance: 用于把 UTC time 转换到选定 timezone。 - - ``string``. Instantiating a ``pytz`` instance will be attempted. + - ``string``: 会尝试实例化对应 ``pytz`` instance。 - - ``integer``. Use, for the strategy, the same timezone as the - corresponding ``data`` in the ``self.datas`` iterable (``0`` would - use the timezone from ``data0``) + - ``integer``: strategy 使用 ``self.datas`` 中对应 ``data`` 的 timezone; + ``0`` 表示使用 ``data0`` 的 timezone。 - ``cheat_on_open`` (default: ``False``) - The ``next_open`` method of strategies will be called. This happens - before ``next`` and before the broker has had a chance to evaluate - orders. The indicators have not yet been recalculated. This allows - issuing an orde which takes into account the indicators of the previous - day but uses the ``open`` price for stake calculations + 会调用 strategy 的 ``next_open`` 方法。它发生在 ``next`` 之前,也发生在 broker + 有机会评估 orders 之前。此时 indicators 尚未重新计算,因此可以基于上一日 + indicators,同时使用 ``open`` price 进行 stake 计算并发出 order。 - For cheat_on_open order execution, it is also necessary to make the - call ``cerebro.broker.set_coo(True)`` or instantite a broker with - ``BackBroker(coo=True)`` (where *coo* stands for cheat-on-open) or set - the ``broker_coo`` parameter to ``True``. Cerebro will do it - automatically unless disabled below. + 对 cheat_on_open order execution,还需要调用 ``cerebro.broker.set_coo(True)``, + 或实例化 ``BackBroker(coo=True)``(*coo* 表示 cheat-on-open),或将 + ``broker_coo`` 设为 ``True``。除非通过下方参数禁用,Cerebro 会自动完成。 - ``broker_coo`` (default: ``True``) - This will automatically invoke the ``set_coo`` method of the broker - with ``True`` to activate ``cheat_on_open`` execution. Will only do it - if ``cheat_on_open`` is also ``True`` + 当 ``cheat_on_open`` 也为 ``True`` 时,自动调用 broker 的 ``set_coo(True)`` + 以激活 ``cheat_on_open`` execution。 - ``quicknotify`` (default: ``False``) - Broker notifications are delivered right before the delivery of the - *next* prices. For backtesting this has no implications, but with live - brokers a notification can take place long before the bar is - delivered. When set to ``True`` notifications will be delivered as soon - as possible (see ``qcheck`` in live feeds) + Broker notifications 默认会在 *next* prices 交付前送达。对 backtesting 这没有 + 影响,但 live broker 中 notification 可能早于 bar 很久发生。设为 ``True`` 时, + notification 会尽快送达(见 live feeds 中的 ``qcheck``)。 - Set to ``False`` for compatibility. May be changed to ``True`` + 为兼容性默认设为 ``False``;未来可能改为 ``True``。 + + Returns: + Cerebro: 可配置并运行回测/优化的核心引擎。 ''' @@ -302,7 +275,7 @@ def __init__(self): self.datas = list() self.datasbyname = collections.OrderedDict() self.strats = list() - self.optcbs = list() # holds a list of callbacks for opt strategies + self.optcbs = list() # 保存 opt strategies 的 callback 列表 self.observers = list() self.analyzers = list() self.indicators = list() @@ -328,14 +301,12 @@ def __init__(self): @staticmethod def iterize(iterable): - '''Handy function which turns things into things that can be iterated upon - including iterables - ''' + '''把输入元素规范化为可迭代对象。''' niterable = list() for elem in iterable: if isinstance(elem, string_types): elem = (elem,) - elif not isinstance(elem, collectionsAbc.Iterable): # Different functions will be called for different Python versions + elif not isinstance(elem, collectionsAbc.Iterable): # 不同 Python 版本会调用不同函数 elem = (elem,) niterable.append(elem) @@ -343,78 +314,65 @@ def iterize(iterable): return niterable def set_fund_history(self, fund): - ''' - Add a history of orders to be directly executed in the broker for - performance evaluation + '''添加 fund history,用于在 broker 中直接执行并评估 performance。 - - ``fund``: is an iterable (ex: list, tuple, iterator, generator) - in which each element will be also an iterable (with length) with - the following sub-elements (2 formats are possible) + Args: + fund: iterable(例如 list、tuple、iterator、generator)。其中每个元素也应是 + 有长度的 iterable,包含以下子元素: ``[datetime, share_value, net asset value]`` - **Note**: it must be sorted (or produce sorted elements) by - datetime ascending + **注意**:必须按 datetime 升序排序,或生成升序元素。 - where: + 其中: - - ``datetime`` is a python ``date/datetime`` instance or a string - with format YYYY-MM-DD[THH:MM:SS[.us]] where the elements in - brackets are optional - - ``share_value`` is an float/integer - - ``net_asset_value`` is a float/integer + - ``datetime`` 是 Python ``date/datetime`` 实例,或格式为 + YYYY-MM-DD[THH:MM:SS[.us]] 的字符串;方括号中的元素可选。 + - ``share_value`` 是 float/integer。 + - ``net_asset_value`` 是 float/integer。 ''' self._fhistory = fund def add_order_history(self, orders, notify=True): - ''' - Add a history of orders to be directly executed in the broker for - performance evaluation + '''添加 order history,用于在 broker 中直接执行并评估 performance。 - - ``orders``: is an iterable (ex: list, tuple, iterator, generator) - in which each element will be also an iterable (with length) with - the following sub-elements (2 formats are possible) + Args: + orders: iterable(例如 list、tuple、iterator、generator)。其中每个元素也应是 + 有长度的 iterable,包含以下子元素(支持 2 种格式): ``[datetime, size, price]`` or ``[datetime, size, price, data]`` - **Note**: it must be sorted (or produce sorted elements) by - datetime ascending + **注意**:必须按 datetime 升序排序,或生成升序元素。 - where: + 其中: - - ``datetime`` is a python ``date/datetime`` instance or a string - with format YYYY-MM-DD[THH:MM:SS[.us]] where the elements in - brackets are optional - - ``size`` is an integer (positive to *buy*, negative to *sell*) - - ``price`` is a float/integer - - ``data`` if present can take any of the following values + - ``datetime`` 是 Python ``date/datetime`` 实例,或格式为 + YYYY-MM-DD[THH:MM:SS[.us]] 的字符串;方括号中的元素可选。 + - ``size`` 是 integer,正数表示 *buy*,负数表示 *sell*。 + - ``price`` 是 float/integer。 + - ``data`` 如存在,可取以下值: - - *None* - The 1st data feed will be used as target - - *integer* - The data with that index (insertion order in - **Cerebro**) will be used - - *string* - a data with that name, assigned for example with - ``cerebro.addata(data, name=value)``, will be the target + - *None*: 使用第 1 个 data feed 作为 target。 + - *integer*: 使用 **Cerebro** 插入顺序中对应 index 的 data。 + - *string*: 使用对应名称的 data,例如通过 + ``cerebro.adddata(data, name=value)`` 指定的 data。 - - ``notify`` (default: *True*) + notify: 默认 *True*。如果为 ``True``,系统中插入的第 1 个 strategy 会收到 + 根据 ``orders`` 中每条信息创建的 artificial order 通知。 - If ``True`` the 1st strategy inserted in the system will be - notified of the artificial orders created following the information - from each order in ``orders`` - - **Note**: Implicit in the description is the need to add a data feed - which is the target of the orders. This is for example needed by - analyzers which track for example the returns + **注意**:这隐含要求添加作为 orders target 的 data feed。例如跟踪 returns 的 + analyzer 会需要该 data。 ''' self._ohistory.append((orders, notify)) def notify_timer(self, timer, when, *args, **kwargs): - '''Receives a timer notification where ``timer`` is the timer which was - returned by ``add_timer``, and ``when`` is the calling time. ``args`` - and ``kwargs`` are any additional arguments passed to ``add_timer`` + '''接收 timer notification。 + + ``timer`` 是 ``add_timer`` 返回的 timer;``when`` 是调用时间。``args`` 和 + ``kwargs`` 是传给 ``add_timer`` 的附加参数。 - The actual ``when`` time can be later, but the system may have not be - able to call the timer before. This value is the timer value and no the - system time. + 实际调用可能晚于 ``when``,因为系统未必能更早调用 timer。该值表示 timer 目标值, + 而不是系统当前时间。 ''' pass @@ -425,9 +383,7 @@ def _add_timer(self, owner, when, allow=None, tzdata=None, strats=False, cheat=False, *args, **kwargs): - '''Internal method to really create the timer (not started yet) which - can be called by cerebro instances or other objects which can access - cerebro''' + '''创建尚未启动的 timer 的内部方法。''' timer = Timer( tid=len(self._pretimers), @@ -450,12 +406,11 @@ def add_timer(self, when, allow=None, tzdata=None, strats=False, cheat=False, *args, **kwargs): - ''' - Schedules a timer to invoke ``notify_timer`` + '''安排 timer 调用 ``notify_timer``。 - Arguments: + Args: - - ``when``: can be + - ``when``: 可以是: - ``datetime.time`` instance (see below ``tzdata``) - ``bt.timer.SESSION_START`` to reference a session start @@ -463,75 +418,60 @@ def add_timer(self, when, - ``offset`` which must be a ``datetime.timedelta`` instance - Used to offset the value ``when``. It has a meaningful use in - combination with ``SESSION_START`` and ``SESSION_END``, to indicated - things like a timer being called ``15 minutes`` after the session - start. + 用于偏移 ``when``。与 ``SESSION_START`` 和 ``SESSION_END`` 组合时尤其有用, + 例如在 session start 后 ``15 minutes`` 调用 timer。 - ``repeat`` which must be a ``datetime.timedelta`` instance - Indicates if after a 1st call, further calls will be scheduled - within the same session at the scheduled ``repeat`` delta + 表示首次调用后,是否在同一 session 内按 ``repeat`` 间隔继续安排调用。 - Once the timer goes over the end of the session it is reset to the - original value for ``when`` + 一旦 timer 超过 session end,它会 reset 到 ``when`` 的原始值。 - ``weekdays``: a **sorted** iterable with integers indicating on - which days (iso codes, Monday is 1, Sunday is 7) the timers can - be actually invoked + which days(ISO code,Monday 为 1,Sunday 为 7)timer 可实际调用。 - If not specified, the timer will be active on all days + 未指定时,timer 对所有日期有效。 - - ``weekcarry`` (default: ``False``). If ``True`` and the weekday was - not seen (ex: trading holiday), the timer will be executed on the - next day (even if in a new week) + - ``weekcarry`` (default: ``False``)。如果为 ``True`` 且指定 weekday 未出现 + (例如 trading holiday),timer 会在下一天执行,即使已经进入新的一周。 - ``monthdays``: a **sorted** iterable with integers indicating on - which days of the month a timer has to be executed. For example - always on day *15* of the month + which days of the month timer 必须执行。例如每月第 *15* 天。 - If not specified, the timer will be active on all days + 未指定时,timer 对所有日期有效。 - - ``monthcarry`` (default: ``True``). If the day was not seen - (weekend, trading holiday), the timer will be executed on the next - available day. + - ``monthcarry`` (default: ``True``)。如果指定日期未出现(weekend、trading + holiday),timer 会在下一个可用日期执行。 - - ``allow`` (default: ``None``). A callback which receives a - `datetime.date`` instance and returns ``True`` if the date is - allowed for timers or else returns ``False`` + - ``allow`` (default: ``None``)。接收 `datetime.date`` 实例的 callback; + 如果该日期允许 timer,则返回 ``True``,否则返回 ``False``。 - ``tzdata`` which can be either ``None`` (default), a ``pytz`` - instance or a ``data feed`` instance. + instance 或 ``data feed`` instance。 - ``None``: ``when`` is interpreted at face value (which translates - to handling it as if it where UTC even if it's not) + ``None``: 按表面值解释 ``when``,即使它不是 UTC,也按类似 UTC 的方式处理。 - ``pytz`` instance: ``when`` will be interpreted as being specified - in the local time specified by the timezone instance. + ``pytz`` instance: ``when`` 会按该 timezone instance 指定的 local time 解释。 - ``data feed`` instance: ``when`` will be interpreted as being - specified in the local time specified by the ``tz`` parameter of - the data feed instance. + ``data feed`` instance: ``when`` 会按该 data feed instance 的 ``tz`` 参数 + 指定的 local time 解释。 - **Note**: If ``when`` is either ``SESSION_START`` or - ``SESSION_END`` and ``tzdata`` is ``None``, the 1st *data feed* - in the system (aka ``self.data0``) will be used as the reference - to find out the session times. + **注意**:如果 ``when`` 是 ``SESSION_START`` 或 ``SESSION_END`` 且 + ``tzdata`` 为 ``None``,系统中的第 1 个 *data feed*(即 ``self.data0``) + 会作为查找 session times 的 reference。 - - ``strats`` (default: ``False``) call also the ``notify_timer`` of - strategies + - ``strats`` (default: ``False``): 是否同时调用 strategies 的 + ``notify_timer``。 - - ``cheat`` (default ``False``) if ``True`` the timer will be called - before the broker has a chance to evaluate the orders. This opens - the chance to issue orders based on opening price for example right - before the session starts - - ``*args``: any extra args will be passed to ``notify_timer`` + - ``cheat`` (default ``False``): 如果为 ``True``,timer 会在 broker 有机会 + 评估 orders 前调用。例如可在 session start 前基于 opening price 发出 order。 + - ``*args``: 额外 args 会传给 ``notify_timer``。 - - ``**kwargs``: any extra kwargs will be passed to ``notify_timer`` + - ``**kwargs``: 额外 kwargs 会传给 ``notify_timer``。 - Return Value: + Returns: - - The created timer + Timer: 创建出的 timer。 ''' return self._add_timer( @@ -543,37 +483,32 @@ def add_timer(self, when, *args, **kwargs) def addtz(self, tz): - ''' - This can also be done with the parameter ``tz`` + '''添加 strategy 使用的全局 timezone。 - Adds a global timezone for strategies. The argument ``tz`` can be + 也可以通过参数 ``tz`` 完成同样配置。``tz`` 可以是: - - ``None``: in this case the datetime displayed by strategies will be - in UTC, which has been always the standard behavior + - ``None``: strategy 显示的 datetime 为 UTC。 - - ``pytz`` instance. It will be used as such to convert UTC times to - the chosen timezone + - ``pytz`` instance: 用于把 UTC times 转换到选定 timezone。 - - ``string``. Instantiating a ``pytz`` instance will be attempted. + - ``string``: 会尝试实例化 ``pytz`` instance。 - - ``integer``. Use, for the strategy, the same timezone as the - corresponding ``data`` in the ``self.datas`` iterable (``0`` would - use the timezone from ``data0``) + - ``integer``: strategy 使用 ``self.datas`` 中对应 ``data`` 的 timezone; + ``0`` 使用 ``data0`` 的 timezone。 ''' self.p.tz = tz def addcalendar(self, cal): - '''Adds a global trading calendar to the system. Individual data feeds - may have separate calendars which override the global one + '''向系统添加全局 trading calendar。 + + 单个 data feed 可拥有独立 calendar,并覆盖全局 calendar。 - ``cal`` can be an instance of ``TradingCalendar`` a string or an - instance of ``pandas_market_calendars``. A string will be will be - instantiated as a ``PandasMarketCalendar`` (which needs the module - ``pandas_market_calendar`` installed in the system. + ``cal`` 可以是 ``TradingCalendar`` 实例、字符串,或 + ``pandas_market_calendars`` 实例。字符串会被实例化为 ``PandasMarketCalendar`` + (需要系统中安装 ``pandas_market_calendar``)。 - If a subclass of `TradingCalendarBase` is passed (not an instance) it - will be instantiated + 如果传入 `TradingCalendarBase` 子类而不是实例,该子类会被实例化。 ''' if isinstance(cal, string_types): cal = PandasMarketCalendar(calendar=cal) @@ -590,94 +525,73 @@ def addcalendar(self, cal): self._tradingcal = cal def add_signal(self, sigtype, sigcls, *sigargs, **sigkwargs): - '''Adds a signal to the system which will be later added to a - ``SignalStrategy``''' + '''添加 signal,稍后会交给 ``SignalStrategy`` 使用。''' self.signals.append((sigtype, sigcls, sigargs, sigkwargs)) def signal_strategy(self, stratcls, *args, **kwargs): - '''Adds a SignalStrategy subclass which can accept signals''' + '''添加可接收 signals 的 ``SignalStrategy`` 子类。''' self._signal_strat = (stratcls, args, kwargs) def signal_concurrent(self, onoff): - '''If signals are added to the system and the ``concurrent`` value is - set to True, concurrent orders will be allowed''' + '''设置 signal strategy 是否允许 concurrent orders。''' self._signal_concurrent = onoff def signal_accumulate(self, onoff): - '''If signals are added to the system and the ``accumulate`` value is - set to True, entering the market when already in the market, will be - allowed to increase a position''' + '''设置 signal strategy 是否允许已有 position 时继续入场加仓。''' self._signal_accumulate = onoff def addstore(self, store): - '''Adds an ``Store`` instance to the if not already present''' + '''添加 ``Store`` 实例;若已存在则不重复添加。''' if store not in self.stores: self.stores.append(store) def addwriter(self, wrtcls, *args, **kwargs): - '''Adds an ``Writer`` class to the mix. Instantiation will be done at - ``run`` time in cerebro - ''' + '''添加 ``Writer`` class;实例化会在 ``run`` 时由 cerebro 完成。''' self.writers.append((wrtcls, args, kwargs)) def addsizer(self, sizercls, *args, **kwargs): - '''Adds a ``Sizer`` class (and args) which is the default sizer for any - strategy added to cerebro - ''' + '''添加默认 ``Sizer`` class 和参数,供所有 strategy 使用。''' self.sizers[None] = (sizercls, args, kwargs) def addsizer_byidx(self, idx, sizercls, *args, **kwargs): - '''Adds a ``Sizer`` class by idx. This idx is a reference compatible to - the one returned by ``addstrategy``. Only the strategy referenced by - ``idx`` will receive this size + '''按 ``idx`` 添加 ``Sizer`` class。 + + ``idx`` 与 ``addstrategy`` 返回值兼容;只有该 ``idx`` 引用的 strategy 会使用 + 该 sizer。 ''' self.sizers[idx] = (sizercls, args, kwargs) def addindicator(self, indcls, *args, **kwargs): - ''' - Adds an ``Indicator`` class to the mix. Instantiation will be done at - ``run`` time in the passed strategies - ''' + '''添加 ``Indicator`` class;会在 ``run`` 时于传入 strategies 中实例化。''' self.indicators.append((indcls, args, kwargs)) def addanalyzer(self, ancls, *args, **kwargs): - ''' - Adds an ``Analyzer`` class to the mix. Instantiation will be done at - ``run`` time - ''' + '''添加 ``Analyzer`` class;实例化会在 ``run`` 时完成。''' self.analyzers.append((ancls, args, kwargs)) def addobserver(self, obscls, *args, **kwargs): - ''' - Adds an ``Observer`` class to the mix. Instantiation will be done at - ``run`` time - ''' + '''添加 ``Observer`` class;实例化会在 ``run`` 时完成。''' self.observers.append((False, obscls, args, kwargs)) def addobservermulti(self, obscls, *args, **kwargs): - ''' - Adds an ``Observer`` class to the mix. Instantiation will be done at - ``run`` time + '''添加 per-data ``Observer`` class。 - It will be added once per "data" in the system. A use case is a - buy/sell observer which observes individual datas. + 实例化会在 ``run`` 时完成。它会针对系统中的每个 "data" 添加一次。 + 典型用例是观察单个 data 的 buy/sell observer。 - A counter-example is the CashValue, which observes system-wide values + 反例是 CashValue,它观察 system-wide values。 ''' self.observers.append((True, obscls, args, kwargs)) def addstorecb(self, callback): - '''Adds a callback to get messages which would be handled by the - notify_store method + '''添加 callback,用于接收原本会由 ``notify_store`` 处理的消息。 - The signature of the callback must support the following: + callback signature 需支持: - - callback(msg, \*args, \*\*kwargs) + - ``callback(msg, *args, **kwargs)`` - The actual ``msg``, ``*args`` and ``**kwargs`` received are - implementation defined (depend entirely on the *data/broker/store*) but - in general one should expect them to be *printable* to allow for - reception and experimentation. + 实际收到的 ``msg``、``*args`` 和 ``**kwargs`` 由实现定义,完全取决于 + *data/broker/store*;通常应假设它们可 *printable*,以便接收和实验。 ''' self.storecbs.append(callback) @@ -688,14 +602,12 @@ def _notify_store(self, msg, *args, **kwargs): self.notify_store(msg, *args, **kwargs) def notify_store(self, msg, *args, **kwargs): - '''Receive store notifications in cerebro + '''在 cerebro 中接收 store notifications。 - This method can be overridden in ``Cerebro`` subclasses + ``Cerebro`` 子类可以覆盖该方法。 - The actual ``msg``, ``*args`` and ``**kwargs`` received are - implementation defined (depend entirely on the *data/broker/store*) but - in general one should expect them to be *printable* to allow for - reception and experimentation. + 实际收到的 ``msg``、``*args`` 和 ``**kwargs`` 由实现定义,完全取决于 + *data/broker/store*;通常应假设它们可 *printable*,以便接收和实验。 ''' pass @@ -709,17 +621,14 @@ def _storenotify(self): strat.notify_store(msg, *args, **kwargs) def adddatacb(self, callback): - '''Adds a callback to get messages which would be handled by the - notify_data method + '''添加 callback,用于接收原本会由 ``notify_data`` 处理的消息。 - The signature of the callback must support the following: + callback signature 需支持: - - callback(data, status, \*args, \*\*kwargs) + - ``callback(data, status, *args, **kwargs)`` - The actual ``*args`` and ``**kwargs`` received are implementation - defined (depend entirely on the *data/broker/store*) but in general one - should expect them to be *printable* to allow for reception and - experimentation. + 实际收到的 ``*args`` 和 ``**kwargs`` 由实现定义,完全取决于 + *data/broker/store*;通常应假设它们可 *printable*,以便接收和实验。 ''' self.datacbs.append(callback) @@ -738,23 +647,19 @@ def _notify_data(self, data, status, *args, **kwargs): self.notify_data(data, status, *args, **kwargs) def notify_data(self, data, status, *args, **kwargs): - '''Receive data notifications in cerebro + '''在 cerebro 中接收 data notifications。 - This method can be overridden in ``Cerebro`` subclasses + ``Cerebro`` 子类可以覆盖该方法。 - The actual ``*args`` and ``**kwargs`` received are - implementation defined (depend entirely on the *data/broker/store*) but - in general one should expect them to be *printable* to allow for - reception and experimentation. + 实际收到的 ``*args`` 和 ``**kwargs`` 由实现定义,完全取决于 + *data/broker/store*;通常应假设它们可 *printable*,以便接收和实验。 ''' pass def adddata(self, data, name=None): - ''' - Adds a ``Data Feed`` instance to the mix. + '''添加 ``Data Feed`` 实例。 - If ``name`` is not None it will be put into ``data._name`` which is - meant for decoration/plotting purposes. + 如果 ``name`` 非 ``None``,会写入 ``data._name``,用于装饰/绘图用途。 ''' if name is not None: data._name = name @@ -774,13 +679,16 @@ def adddata(self, data, name=None): return data def chaindata(self, *args, **kwargs): - ''' - Chains several data feeds into one + '''将多个 data feeds 串接为一个 data feed。 - If ``name`` is passed as named argument and is not None it will be put - into ``data._name`` which is meant for decoration/plotting purposes. + Args: + - ``*args``: 要串接的 data feed。 + - ``name``: 可选名称;如果提供且非 ``None``,会写入 ``data._name``, + 用于装饰/绘图用途。 - If ``None``, then the name of the 1st data will be used + Returns: + Chainer: 串接后的 data feed;如果未提供 ``name``,使用第 1 个 + data feed 的名称。 ''' dname = kwargs.pop('name', None) if dname is None: @@ -791,15 +699,17 @@ def chaindata(self, *args, **kwargs): return d def rolloverdata(self, *args, **kwargs): - '''Chains several data feeds into one - - If ``name`` is passed as named argument and is not None it will be put - into ``data._name`` which is meant for decoration/plotting purposes. + '''将多个 data feeds 通过 RollOver 串接为一个 data feed。 - If ``None``, then the name of the 1st data will be used - - Any other kwargs will be passed to the RollOver class + Args: + - ``*args``: 要串接的 data feed。 + - ``name``: 可选名称;如果提供且非 ``None``,会写入 ``data._name``, + 用于装饰/绘图用途。 + - ``**kwargs``: 其他参数会传给 ``RollOver`` 类。 + Returns: + RollOver: 串接后的 data feed;如果未提供 ``name``,使用第 1 个 + data feed 的名称。 ''' dname = kwargs.pop('name', None) if dname is None: @@ -810,14 +720,17 @@ def rolloverdata(self, *args, **kwargs): return d def replaydata(self, dataname, name=None, **kwargs): - ''' - Adds a ``Data Feed`` to be replayed by the system + '''添加一个由系统 replay 的 ``Data Feed``。 - If ``name`` is not None it will be put into ``data._name`` which is - meant for decoration/plotting purposes. + Args: + - ``dataname``: 要 replay 的 data feed;如果它已在系统中,会先 clone。 + - ``name``: 可选名称;如果非 ``None``,会写入 ``data._name``, + 用于装饰/绘图用途。 + - ``**kwargs``: ``timeframe``、``compression``、``todate`` 等 replay + filter 支持的参数会透明传递。 - Any other kwargs like ``timeframe``, ``compression``, ``todate`` which - are supported by the replay filter will be passed transparently + Returns: + DataBase: 已配置 replay 的 data feed。 ''' if any(dataname is x for x in self.datas): dataname = dataname.clone() @@ -829,14 +742,17 @@ def replaydata(self, dataname, name=None, **kwargs): return dataname def resampledata(self, dataname, name=None, **kwargs): - ''' - Adds a ``Data Feed`` to be resample by the system + '''添加一个由系统 resample 的 ``Data Feed``。 - If ``name`` is not None it will be put into ``data._name`` which is - meant for decoration/plotting purposes. + Args: + - ``dataname``: 要 resample 的 data feed;如果它已在系统中,会先 clone。 + - ``name``: 可选名称;如果非 ``None``,会写入 ``data._name``, + 用于装饰/绘图用途。 + - ``**kwargs``: ``timeframe``、``compression``、``todate`` 等 resample + filter 支持的参数会透明传递。 - Any other kwargs like ``timeframe``, ``compression``, ``todate`` which - are supported by the resample filter will be passed transparently + Returns: + DataBase: 已配置 resample 的 data feed。 ''' if any(dataname is x for x in self.datas): dataname = dataname.clone() @@ -848,46 +764,42 @@ def resampledata(self, dataname, name=None, **kwargs): return dataname def optcallback(self, cb): - ''' - Adds a *callback* to the list of callbacks that will be called with the - optimizations when each of the strategies has been run + '''添加 optimization 回调函数。 + + 每组 strategy 运行完成后,回调会随 optimization 结果一起被调用。 - The signature: cb(strategy) + Args: + - ``cb``: 回调函数,签名为 ``cb(strategy)``。 ''' self.optcbs.append(cb) def optstrategy(self, strategy, *args, **kwargs): - ''' - Adds a ``Strategy`` class to the mix for optimization. Instantiation - will happen during ``run`` time. - - args and kwargs MUST BE iterables which hold the values to check. - - Example: if a Strategy accepts a parameter ``period``, for optimization - purposes the call to ``optstrategy`` looks like: - - - cerebro.optstrategy(MyStrategy, period=(15, 25)) - - This will execute an optimization for values 15 and 25. Whereas - - - cerebro.optstrategy(MyStrategy, period=range(15, 25)) + '''添加用于 optimization 的 ``Strategy`` 类。 - will execute MyStrategy with ``period`` values 15 -> 25 (25 not - included, because ranges are semi-open in Python) + strategy 会在 ``run`` 阶段实例化。``args`` 和 ``kwargs`` 必须是 + iterable,用于保存要检查的参数值。 - If a parameter is passed but shall not be optimized the call looks - like: + Args: + - ``strategy``: 要优化的 ``Strategy`` 类。 + - ``*args``: 位置参数候选值,每个参数都应为 iterable。 + - ``**kwargs``: 关键字参数候选值,每个参数都应为 iterable。 - - cerebro.optstrategy(MyStrategy, period=(15,)) + --- + 交互界面使用示范例子: - Notice that ``period`` is still passed as an iterable ... of just 1 - element + - 如果 strategy 接受 ``period`` 参数,可调用 + ``cerebro.optstrategy(MyStrategy, period=(15, 25))``,系统会分别 + 使用 ``15`` 和 ``25`` 执行 optimization。 - ``backtrader`` will anyhow try to identify situations like: + - ``cerebro.optstrategy(MyStrategy, period=range(15, 25))`` 会依次 + 尝试 ``15`` 到 ``24``;Python 的 ``range`` 是半开区间,不包含 + 终点 ``25``。 - - cerebro.optstrategy(MyStrategy, period=15) + - 如果某个参数不需要优化,也仍然用单元素 iterable,例如 + ``cerebro.optstrategy(MyStrategy, period=(15,))``。 - and will create an internal pseudo-iterable if possible + - ``backtrader`` 会尽量识别 ``period=15`` 这类写法,并在可行时 + 内部构造伪 iterable。 ''' self._dooptimize = True args = self.iterize(args) @@ -906,33 +818,40 @@ def optstrategy(self, strategy, *args, **kwargs): self.strats.append(it) def addstrategy(self, strategy, *args, **kwargs): - ''' - Adds a ``Strategy`` class to the mix for a single pass run. - Instantiation will happen during ``run`` time. + '''添加用于单次运行的 ``Strategy`` 类。 + + strategy 会在 ``run`` 阶段实例化,``args`` 和 ``kwargs`` 会原样传给 + strategy 构造函数。 - args and kwargs will be passed to the strategy as they are during - instantiation. + Args: + - ``strategy``: 要运行的 ``Strategy`` 类。 + - ``*args``: 传给 strategy 的位置参数。 + - ``**kwargs``: 传给 strategy 的关键字参数。 - Returns the index with which addition of other objects (like sizers) - can be referenced + Returns: + int: 本次添加的索引,可供后续添加其他对象(例如 sizers)时引用。 ''' self.strats.append([(strategy, args, kwargs)]) return len(self.strats) - 1 def setbroker(self, broker): - ''' - Sets a specific ``broker`` instance for this strategy, replacing the - one inherited from cerebro. + '''设置当前 Cerebro 使用的 ``broker`` 实例。 + + Args: + - ``broker``: 要绑定到当前 Cerebro 的 broker 实例。 + + Returns: + BrokerBase: 传入的 broker 实例。 ''' self._broker = broker broker.cerebro = self return broker def getbroker(self): - ''' - Returns the broker instance. + '''返回当前 broker 实例。 - This is also available as a ``property`` by the name ``broker`` + Returns: + BrokerBase: 当前 broker 实例;也可以通过 ``broker`` property 访问。 ''' return self._broker @@ -941,36 +860,27 @@ def getbroker(self): def plot(self, plotter=None, numfigs=1, iplot=True, start=None, end=None, width=16, height=9, dpi=300, tight=True, use=None, **kwargs): - ''' - Plots the strategies inside cerebro - - If ``plotter`` is None a default ``Plot`` instance is created and - ``kwargs`` are passed to it during instantiation. - - ``numfigs`` split the plot in the indicated number of charts reducing - chart density if wished - - ``iplot``: if ``True`` and running in a ``notebook`` the charts will be - displayed inline - - ``use``: set it to the name of the desired matplotlib backend. It will - take precedence over ``iplot`` - - ``start``: An index to the datetime line array of the strategy or a - ``datetime.date``, ``datetime.datetime`` instance indicating the start - of the plot - - ``end``: An index to the datetime line array of the strategy or a - ``datetime.date``, ``datetime.datetime`` instance indicating the end - of the plot - - ``width``: in inches of the saved figure - - ``height``: in inches of the saved figure - - ``dpi``: quality in dots per inches of the saved figure - - ``tight``: only save actual content and not the frame of the figure + '''绘制 Cerebro 中的 strategies。 + + Args: + - ``plotter``: 可选 plotter;如果为 ``None``,会创建默认 ``Plot`` + 实例,并把 ``kwargs`` 传给它。 + - ``numfigs``: 将图形拆成多少张 chart,可用于降低单张图密度。 + - ``iplot``: 在 notebook 中是否 inline 显示图形。 + - ``start``: 绘图起点,可为 strategy datetime line 的索引, + 或 ``datetime.date`` / ``datetime.datetime`` 实例。 + - ``end``: 绘图终点,可为 strategy datetime line 的索引, + 或 ``datetime.date`` / ``datetime.datetime`` 实例。 + - ``width``: 保存图像的宽度,单位为 inch。 + - ``height``: 保存图像的高度,单位为 inch。 + - ``dpi``: 保存图像的 dots per inch 质量。 + - ``tight``: 是否只保存实际内容而不包含 figure 边框。 + - ``use``: 指定 matplotlib backend;优先级高于 ``iplot``。 + - ``**kwargs``: 创建默认 plotter 时传入的额外参数。 + + Returns: + list: 每个 strategy 绘制出的 figure 列表;如果启用 ``exactbars``, + 不执行绘图。 ''' if self._exactbars > 0: return @@ -1003,19 +913,13 @@ def plot(self, plotter=None, numfigs=1, iplot=True, start=None, end=None, return figs def __call__(self, iterstrat): - ''' - Used during optimization to pass the cerebro over the multiprocesing - module without complains - ''' + '''optimization 时供 multiprocessing 调用当前 Cerebro 实例。''' predata = self.p.optdatas and self._dopreload and self._dorunonce return self.runstrategies(iterstrat, predata=predata) def __getstate__(self): - ''' - Used during optimization to prevent optimization result `runstrats` - from being pickled to subprocesses - ''' + '''optimization 时避免把结果 ``runstrats`` pickle 到子进程。''' rv = vars(self).copy() if 'runstrats' in rv: @@ -1023,38 +927,37 @@ def __getstate__(self): return rv def runstop(self): - '''If invoked from inside a strategy or anywhere else, including other - threads the execution will stop as soon as possible.''' - self._event_stop = True # signal a stop has been requested + '''请求尽快停止运行。 - def run(self, **kwargs): - '''The core method to perform backtesting. Any ``kwargs`` passed to it - will affect the value of the standard parameters ``Cerebro`` was - instantiated with. + 可从 strategy 内部、其他位置甚至其他线程调用。 + ''' + self._event_stop = True # 标记已经请求 stop - If ``cerebro`` has not datas the method will immediately bail out. + def run(self, **kwargs): + '''执行 backtesting 的核心方法。 - It has different return values: + Args: + - ``**kwargs``: 覆盖 Cerebro 初始化时使用的标准参数。 - - For No Optimization: a list contanining instances of the Strategy - classes added with ``addstrategy`` + Returns: + list: 未启用 optimization 时,返回通过 ``addstrategy`` 添加的 + ``Strategy`` 实例列表;启用 optimization 时,返回包含这些列表的列表。 - - For Optimization: a list of lists which contain instances of the - Strategy classes added with ``addstrategy`` + 如果没有 data,方法会立即返回空列表。 ''' - self._event_stop = False # Stop is requested + self._event_stop = False # 尚未请求 stop if not self.datas: - return [] # nothing can be run + return [] # 没有可运行内容 pkeys = self.params._getkeys() for key, val in kwargs.items(): if key in pkeys: setattr(self.params, key, val) - # Manage activate/deactivate object cache - linebuffer.LineActions.cleancache() # clean cache - indicator.Indicator.cleancache() # clean cache + # 管理对象 cache 的启用/停用 + linebuffer.LineActions.cleancache() # 清理 cache + indicator.Indicator.cleancache() # 清理 cache linebuffer.LineActions.usecache(self.p.objcache) indicator.Indicator.usecache(self.p.objcache) @@ -1064,33 +967,32 @@ def run(self, **kwargs): self._exactbars = int(self.p.exactbars) if self._exactbars: - self._dorunonce = False # something is saving memory, no runonce + self._dorunonce = False # 启用内存节省模式时不使用 runonce self._dopreload = self._dopreload and self._exactbars < 1 self._doreplay = self._doreplay or any(x.replaying for x in self.datas) if self._doreplay: - # preloading is not supported with replay. full timeframe bars - # are constructed in realtime + # replay 不支持 preloading;完整 timeframe bar 会实时构造 self._dopreload = False if self._dolive or self.p.live: - # in this case both preload and runonce must be off + # live 模式下 preload 和 runonce 都必须关闭 self._dorunonce = False self._dopreload = False self.runwriters = list() - # Add the system default writer if requested + # 按需添加系统默认 writer if self.p.writer is True: wr = WriterFile() self.runwriters.append(wr) - # Instantiate any other writers + # 实例化其他 writers for wrcls, wrargs, wrkwargs in self.writers: wr = wrcls(*wrargs, **wrkwargs) self.runwriters.append(wr) - # Write down if any writer wants the full csv output + # 记录是否有 writer 需要完整 csv 输出 self.writers_csv = any(map(lambda x: x.p.csv, self.runwriters)) self.runstrats = list() @@ -1098,22 +1000,22 @@ def run(self, **kwargs): if self.signals: # allow processing of signals signalst, sargs, skwargs = self._signal_strat if signalst is None: - # Try to see if the 1st regular strategy is a signal strategy + # 尝试判断第 1 个常规 strategy 是否为 signal strategy try: signalst, sargs, skwargs = self.strats.pop(0) except IndexError: - pass # Nothing there + pass # 没有可取出的 strategy else: if not isinstance(signalst, SignalStrategy): - # no signal ... reinsert at the beginning + # 不是 signal strategy,重新插回开头 self.strats.insert(0, (signalst, sargs, skwargs)) - signalst = None # flag as not presetn + signalst = None # 标记为未预设 if signalst is None: # recheck - # Still None, create a default one + # 仍然为 None,则创建默认 signal strategy signalst, sargs, skwargs = SignalStrategy, tuple(), dict() - # Add the signal strategy + # 添加 signal strategy self.addstrategy(signalst, _accumulate=self._signal_accumulate, _concurrent=self._signal_concurrent, @@ -1121,24 +1023,23 @@ def run(self, **kwargs): *sargs, **skwargs) - if not self.strats: # Datas are present, add a strategy + if not self.strats: # 已有 datas 时,添加默认 strategy self.addstrategy(Strategy) iterstrats = itertools.product(*self.strats) if not self._dooptimize or self.p.maxcpus == 1: - # If no optimmization is wished ... or 1 core is to be used - # let's skip process "spawning" + # 未请求 optimization 或只使用 1 个核心时,跳过进程派生 for iterstrat in iterstrats: runstrat = self.runstrategies(iterstrat) self.runstrats.append(runstrat) if self._dooptimize: for cb in self.optcbs: - cb(runstrat) # callback receives finished strategy + cb(runstrat) # callback 接收已完成的 strategy else: if self.p.optdatas and self._dopreload and self._dorunonce: for data in self.datas: data.reset() - if self._exactbars < 1: # datas can be full length + if self._exactbars < 1: # datas 可以保留完整长度 data.extend(size=self.params.lookahead) data._start() if self._dopreload: @@ -1148,7 +1049,7 @@ def run(self, **kwargs): for r in pool.imap(self, iterstrats): self.runstrats.append(r) for cb in self.optcbs: - cb(r) # callback receives finished strategy + cb(r) # callback 接收已完成的 strategy pool.close() @@ -1157,7 +1058,7 @@ def run(self, **kwargs): data.stop() if not self._dooptimize: - # avoid a list of list for regular cases + # 常规运行避免返回 list of list return self.runstrats[0] return self.runstrats @@ -1169,9 +1070,7 @@ def _next_stid(self): return next(self.stcount) def runstrategies(self, iterstrat, predata=False): - ''' - Internal method invoked by ``run``` to run a set of strategies - ''' + '''由 ``run`` 调用的内部方法,用于运行一组 strategies。''' self._init_stcount() self.runningstrats = runstrats = list() @@ -1179,7 +1078,7 @@ def runstrategies(self, iterstrat, predata=False): store.start() if self.p.cheat_on_open and self.p.broker_coo: - # try to activate in broker + # 尝试在 broker 中启用 cheat-on-open if hasattr(self._broker, 'set_coo'): self._broker.set_coo(True) @@ -1210,7 +1109,7 @@ def runstrategies(self, iterstrat, predata=False): if not predata: for data in self.datas: data.reset() - if self._exactbars < 1: # datas can be full length + if self._exactbars < 1: # datas 可以保留完整长度 data.extend(size=self.params.lookahead) data._start() if self._dopreload: @@ -1221,10 +1120,10 @@ def runstrategies(self, iterstrat, predata=False): try: strat = stratcls(*sargs, **skwargs) except bt.errors.StrategySkipError: - continue # do not add strategy to the mix + continue # 不把该 strategy 加入运行集合 if self.p.oldsync: - strat._oldsync = True # tell strategy to use old clock update + strat._oldsync = True # 告知 strategy 使用旧式时钟更新 if self.p.tradehistory: strat.set_tradehistory() runstrats.append(strat) @@ -1236,7 +1135,7 @@ def runstrategies(self, iterstrat, predata=False): tz = tzparse(tz) if runstrats: - # loop separated for clarity + # 为了清晰起见,单独组织循环 defaultsizer = self.sizers.get(None, (None, None, None)) for idx, strat in enumerate(runstrats): if self.p.stdstats: @@ -1279,11 +1178,11 @@ def runstrategies(self, iterstrat, predata=False): for writer in self.runwriters: writer.start() - # Prepare timers + # 准备 timers self._timers = [] self._timerscheat = [] for timer in self._pretimers: - # preprocess tzdata if needed + # 按需预处理 tzdata timer.start(self.datas[0]) if timer.params.cheat: @@ -1320,7 +1219,7 @@ def runstrategies(self, iterstrat, predata=False): self.stop_writers(runstrats) if self._dooptimize and self.p.optreturn: - # Results can be optimized + # optimization 结果可以被压缩为轻量返回对象 results = list() for strat in runstrats: for a in strat.analyzers: @@ -1358,10 +1257,7 @@ def stop_writers(self, runstrats): writer.stop() def _brokernotify(self): - ''' - Internal method which kicks the broker and delivers any broker - notification to the strategy - ''' + '''驱动 broker,并把 broker 通知分发给 strategy。''' self._broker.next() while True: order = self._broker.get_notification() @@ -1370,39 +1266,37 @@ def _brokernotify(self): owner = order.owner if owner is None: - owner = self.runningstrats[0] # default + owner = self.runningstrats[0] # 默认归属 owner._addnotification(order, quicknotify=self.p.quicknotify) def _runnext_old(self, runstrats): - ''' - Actual implementation of run in full next mode. All objects have its - ``next`` method invoke on each data arrival + '''旧同步模式下 full next 运行的实际实现。 + + 每次 data 到达时,所有对象都会调用自己的 ``next`` 方法。 ''' data0 = self.datas[0] d0ret = True while d0ret or d0ret is None: lastret = False - # Notify anything from the store even before moving datas - # because datas may not move due to an error reported by the store + # 在移动 datas 前先分发 store 通知,因为 store 报错可能导致 datas 不移动 self._storenotify() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return self._datanotify() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return d0ret = data0.next() if d0ret: for data in self.datas[1:]: - if not data.next(datamaster=data0): # no delivery - data._check(forcedata=data0) # check forcing output - data.next(datamaster=data0) # retry + if not data.next(datamaster=data0): # 没有输出 + data._check(forcedata=data0) # 检查是否强制输出 + data.next(datamaster=data0) # 重试 elif d0ret is None: - # meant for things like live feeds which may not produce a bar - # at the moment but need the loop to run for notifications and - # getting resample and others to produce timely bars + # live feeds 之类的数据源可能暂时不产生 bar,但仍需要循环继续, + # 以便处理通知,并让 resample 等逻辑及时产出 bars data0._check() for data in self.datas[1:]: data._check() @@ -1412,47 +1306,44 @@ def _runnext_old(self, runstrats): lastret += data._last(datamaster=data0) if not lastret: - # Only go extra round if something was changed by "lasts" + # 只有 "lasts" 改变了内容时,才额外运行一轮 break - # Datas may have generated a new notification after next + # datas 在 next 后可能生成了新通知 self._datanotify() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return self._brokernotify() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return - if d0ret or lastret: # bars produced by data or filters + if d0ret or lastret: # data 或 filters 产出了 bars for strat in runstrats: strat._next() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return self._next_writers(runstrats) - # Last notification chance before stopping + # 停止前最后一次处理通知 self._datanotify() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return self._storenotify() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return def _runonce_old(self, runstrats): - ''' - Actual implementation of run in vector mode. - Strategies are still invoked on a pseudo-event mode in which ``next`` - is called for each data arrival + '''旧同步模式下 vector 运行的实际实现。 + + strategies 仍然通过伪事件模式调用,每次 data 到达时触发 ``next``。 ''' for strat in runstrats: strat._once() - # The default once for strategies does nothing and therefore - # has not moved forward all datas/indicators/observers that - # were homed before calling once, Hence no "need" to do it - # here again, because pointers are at 0 + # strategy 默认的 once 不做任何事,因此不会推进调用 once 前归位的 + # datas/indicators/observers;这里也无需再次推进,因为指针仍在 0 data0 = self.datas[0] datas = self.datas[1:] for i in range(data0.buflen()): @@ -1461,13 +1352,13 @@ def _runonce_old(self, runstrats): data.advance(datamaster=data0) self._brokernotify() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return for strat in runstrats: - # data0.datetime[0] for compat. w/ new strategy's oncepost + # data0.datetime[0] 用于兼容新版 strategy 的 oncepost strat._oncepost(data0.datetime[0]) - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return self._next_writers(runstrats) @@ -1492,13 +1383,13 @@ def _next_writers(self, runstrats): writer.next() def _disable_runonce(self): - '''API for lineiterators to disable runonce (see HeikinAshi)''' + '''供 lineiterators 禁用 runonce 的 API(参见 HeikinAshi)。''' self._dorunonce = False def _runnext(self, runstrats): - ''' - Actual implementation of run in full next mode. All objects have its - ``next`` method invoke on each data arrival + '''full next 模式下运行的实际实现。 + + 每次 data 到达时,所有对象都会调用自己的 ``next`` 方法。 ''' datas = sorted(self.datas, key=lambda x: (x._timeframe, x._compression)) @@ -1517,28 +1408,25 @@ def _runnext(self, runstrats): ldatas = len(datas) ldatas_noclones = ldatas - clonecount lastqcheck = False - dt0 = date2num(datetime.datetime.max) - 2 # default at max + dt0 = date2num(datetime.datetime.max) - 2 # 默认接近最大时间 while d0ret or d0ret is None: - # if any has live data in the buffer, no data will wait anything + # 如果任一 data 在 buffer 中有 live data,则所有 data 都不等待 newqcheck = not any(d.haslivedata() for d in datas) if not newqcheck: - # If no data has reached the live status or all, wait for - # the next incoming data + # 如果没有 data 进入 live 状态,或全部已进入,则等待下一批 data livecount = sum(d._laststatus == d.LIVE for d in datas) newqcheck = not livecount or livecount == ldatas_noclones lastret = False - # Notify anything from the store even before moving datas - # because datas may not move due to an error reported by the store + # 在移动 datas 前先分发 store 通知,因为 store 报错可能导致 datas 不移动 self._storenotify() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return self._datanotify() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return - # record starting time and tell feeds to discount the elapsed time - # from the qcheck value + # 记录起始时间,并告知 feeds 从 qcheck 中扣除已耗费时间 drets = [] qstart = datetime.datetime.utcnow() for d in datas: @@ -1555,52 +1443,51 @@ def _runnext(self, runstrats): for i, ret in enumerate(drets): dts.append(datas[i].datetime[0] if ret else None) - # Get index to minimum datetime + # 找到最小 datetime 的索引 if onlyresample or noresample: dt0 = min((d for d in dts if d is not None)) else: dt0 = min((d for i, d in enumerate(dts) if d is not None and i not in rsonly)) - dmaster = datas[dts.index(dt0)] # and timemaster + dmaster = datas[dts.index(dt0)] # 同时作为 timemaster self._dtmaster = dmaster.num2date(dt0) self._udtmaster = num2date(dt0) # slen = len(runstrats[0]) - # Try to get something for those that didn't return + # 尝试为没有返回的 data 取得输出 for i, ret in enumerate(drets): - if ret: # dts already contains a valid datetime for this i + if ret: # dts 已包含该索引的有效 datetime continue - # try to get a data by checking with a master + # 通过 master 检查,尝试让 data 输出 d = datas[i] - d._check(forcedata=dmaster) # check to force output - if d.next(datamaster=dmaster, ticks=False): # retry - dts[i] = d.datetime[0] # good -> store - # self._plotfillers2[i].append(slen) # mark as fill + d._check(forcedata=dmaster) # 检查是否强制输出 + if d.next(datamaster=dmaster, ticks=False): # 重试 + dts[i] = d.datetime[0] # 成功则保存 + # self._plotfillers2[i].append(slen) # 标记为填充 else: - # self._plotfillers[i].append(slen) # mark as empty + # self._plotfillers[i].append(slen) # 标记为空 pass - # make sure only those at dmaster level end up delivering + # 确保最终只有处于 dmaster 层级的 data 交付输出 for i, dti in enumerate(dts): if dti is not None: di = datas[i] - rpi = False and di.replaying # to check behavior + rpi = False and di.replaying # 用于检查行为 if dti > dt0: - if not rpi: # must see all ticks ... - di.rewind() # cannot deliver yet + if not rpi: # 必须看到所有 ticks + di.rewind() # 还不能交付 # self._plotfillers[i].append(slen) elif not di.replaying: - # Replay forces tick fill, else force here + # replay 会强制 tick fill,否则在这里强制 di._tick_fill(force=True) - # self._plotfillers2[i].append(slen) # mark as fill + # self._plotfillers2[i].append(slen) # 标记为填充 elif d0ret is None: - # meant for things like live feeds which may not produce a bar - # at the moment but need the loop to run for notifications and - # getting resample and others to produce timely bars + # live feeds 之类的数据源可能暂时不产生 bar,但仍需要循环继续, + # 以便处理通知,并让 resample 等逻辑及时产出 bars for data in datas: data._check() else: @@ -1609,75 +1496,71 @@ def _runnext(self, runstrats): lastret += data._last(datamaster=data0) if not lastret: - # Only go extra round if something was changed by "lasts" + # 只有 "lasts" 改变了内容时,才额外运行一轮 break - # Datas may have generated a new notification after next + # datas 在 next 后可能生成了新通知 self._datanotify() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return - if d0ret or lastret: # if any bar, check timers before broker + if d0ret or lastret: # 只要有 bar,就在 broker 前检查 timers self._check_timers(runstrats, dt0, cheat=True) if self.p.cheat_on_open: for strat in runstrats: strat._next_open() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return self._brokernotify() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return - if d0ret or lastret: # bars produced by data or filters + if d0ret or lastret: # data 或 filters 产出了 bars self._check_timers(runstrats, dt0, cheat=False) for strat in runstrats: strat._next() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return self._next_writers(runstrats) - # Last notification chance before stopping + # 停止前最后一次处理通知 self._datanotify() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return self._storenotify() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return def _runonce(self, runstrats): - ''' - Actual implementation of run in vector mode. + '''vector 模式下运行的实际实现。 - Strategies are still invoked on a pseudo-event mode in which ``next`` - is called for each data arrival + strategies 仍然通过伪事件模式调用,每次 data 到达时触发 ``next``。 ''' for strat in runstrats: strat._once() - strat.reset() # strat called next by next - reset lines + strat.reset() # strategy 接下来会被 next 调用,重置 lines - # The default once for strategies does nothing and therefore - # has not moved forward all datas/indicators/observers that - # were homed before calling once, Hence no "need" to do it - # here again, because pointers are at 0 + # strategy 默认的 once 不做任何事,因此不会推进调用 once 前归位的 + # datas/indicators/observers;这里也无需再次推进,因为指针仍在 0 datas = sorted(self.datas, key=lambda x: (x._timeframe, x._compression)) while True: - # Check next incoming date in the datas + # 检查 datas 中下一个到达日期 dts = [d.advance_peek() for d in datas] dt0 = min(dts) if dt0 == float('inf'): - break # no data delivers anything + break # 没有 data 会继续交付内容 - # Timemaster if needed be - # dmaster = datas[dts.index(dt0)] # and timemaster + # 按需获取 timemaster + # dmaster = datas[dts.index(dt0)] # 同时作为 timemaster slen = len(runstrats[0]) for i, dti in enumerate(dts): if dti <= dt0: datas[i].advance() - # self._plotfillers2[i].append(slen) # mark as fill + # self._plotfillers2[i].append(slen) # 标记为填充 else: # self._plotfillers[i].append(slen) pass @@ -1687,18 +1570,18 @@ def _runonce(self, runstrats): if self.p.cheat_on_open: for strat in runstrats: strat._oncepost_open() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return self._brokernotify() - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return self._check_timers(runstrats, dt0, cheat=False) for strat in runstrats: strat._oncepost(dt0) - if self._event_stop: # stop if requested + if self._event_stop: # 如果已请求 stop return self._next_writers(runstrats) diff --git a/backtrader/comminfo.py b/backtrader/comminfo.py index 6bfd8e015..d7aa24e34 100644 --- a/backtrader/comminfo.py +++ b/backtrader/comminfo.py @@ -28,92 +28,57 @@ class CommInfoBase(with_metaclass(MetaParams)): - '''Base Class for the Commission Schemes. - - Params: - - - ``commission`` (def: ``0.0``): base commission value in percentage or - monetary units - - - ``mult`` (def ``1.0``): multiplier applied to the asset for - value/profit - - - ``margin`` (def: ``None``): amount of monetary units needed to - open/hold an operation. It only applies if the final ``_stocklike`` - attribute in the class is set to ``False`` - - - ``automargin`` (def: ``False``): Used by the method ``get_margin`` - to automatically calculate the margin/guarantees needed with the - following policy - - - Use param ``margin`` if param ``automargin`` evaluates to ``False`` - - - Use param ``mult`` * ``price`` if ``automargin < 0`` - - - Use param ``automargin`` * ``price`` if ``automargin > 0`` - - - ``commtype`` (def: ``None``): Supported values are - ``CommInfoBase.COMM_PERC`` (commission to be understood as %) and - ``CommInfoBase.COMM_FIXED`` (commission to be understood as monetary - units) - - The default value of ``None`` is a supported value to retain - compatibility with the legacy ``CommissionInfo`` object. If - ``commtype`` is set to None, then the following applies: - - - ``margin`` is ``None``: Internal ``_commtype`` is set to - ``COMM_PERC`` and ``_stocklike`` is set to ``True`` (Operating - %-wise with Stocks) - - - ``margin`` is not ``None``: ``_commtype`` set to ``COMM_FIXED`` and - ``_stocklike`` set to ``False`` (Operating with fixed rount-trip - commission with Futures) - - If this param is set to something else than ``None``, then it will be - passed to the internal ``_commtype`` attribute and the same will be - done with the param ``stocklike`` and the internal attribute - ``_stocklike`` - - - ``stocklike`` (def: ``False``): Indicates if the instrument is - Stock-like or Futures-like (see the ``commtype`` discussion above) - - - ``percabs`` (def: ``False``): when ``commtype`` is set to COMM_PERC, - whether the parameter ``commission`` has to be understood as XX% or - 0.XX - - If this param is ``True``: 0.XX - If this param is ``False``: XX% - - - ``interest`` (def: ``0.0``) - - If this is non-zero, this is the yearly interest charged for holding a - short selling position. This is mostly meant for stock short-selling - - The formula: ``days * price * abs(size) * (interest / 365)`` - - It must be specified in absolute terms: 0.05 -> 5% - - .. note:: the behavior can be changed by overriding the method: - ``_get_credit_interest`` - - - ``interest_long`` (def: ``False``) - - Some products like ETFs get charged on interest for short and long - positions. If ths is ``True`` and ``interest`` is non-zero the interest - will be charged on both directions - - - ``leverage`` (def: ``1.0``) - - Amount of leverage for the asset with regards to the needed cash + '''Commission Schemes 的基类,用于描述资产的 commission、margin、leverage + 和 credit interest 计算规则。 + + Args: + commission (float): 基础 commission 数值,可以表示百分比或货币单位。 + mult (float): 用于 asset value/profit 的乘数。 + margin: 打开/持有一次操作所需的货币单位数量。仅当最终 ``_stocklike`` + 属性为 ``False`` 时适用。 + automargin: 由 ``get_margin`` 用于自动计算所需 margin/guarantees。 + 规则如下: + + - 如果 ``automargin`` 为 ``False``,使用参数 ``margin`` + - 如果 ``automargin < 0``,使用 ``mult * price`` + - 如果 ``automargin > 0``,使用 ``automargin * price`` + + commtype: 支持 ``CommInfoBase.COMM_PERC`` 和 + ``CommInfoBase.COMM_FIXED``。``COMM_PERC`` 表示 commission 按 + 百分比理解,``COMM_FIXED`` 表示 commission 按货币单位理解。 + + ``None`` 是受支持的默认值,用于保持与旧版 ``CommissionInfo`` 对象 + 兼容。如果 ``commtype`` 为 ``None``,则: + + - ``margin`` 为 ``None``: 内部 ``_commtype`` 设为 ``COMM_PERC``, + ``_stocklike`` 设为 ``True``(按股票式百分比运作) + - ``margin`` 不为 ``None``: 内部 ``_commtype`` 设为 + ``COMM_FIXED``,``_stocklike`` 设为 ``False``(按 futures 式固定 + round-trip commission 运作) + + 如果该参数不是 ``None``,则会赋值给内部 ``_commtype`` 属性;参数 + ``stocklike`` 也会同样赋值给内部 ``_stocklike`` 属性。 + + stocklike (bool): 表示 instrument 是 Stock-like 还是 Futures-like + (见上方 ``commtype`` 说明)。 + percabs (bool): 当 ``commtype`` 为 ``COMM_PERC`` 时,表示参数 + ``commission`` 应按 XX% 还是 0.XX 理解。``True`` 表示 0.XX, + ``False`` 表示 XX%。 + interest (float): 持有 short selling position 时收取的年化 interest。 + 主要用于股票 short-selling。公式为 + ``days * price * abs(size) * (interest / 365)``。必须按绝对值指定: + 0.05 表示 5%。 + interest_long (bool): 某些产品(如 ETF)会对 short 和 long position + 都收取 interest。如果该值为 ``True`` 且 ``interest`` 非零,则两个 + 方向都会收取 interest。 + leverage (float): 该 asset 相对于所需 cash 的 leverage。 Attributes: + _stocklike: 最终用于 Stock-like/Futures-like 行为的值。 + _commtype: 最终用于 PERC/FIXED commission 行为的值。 - - ``_stocklike``: Final value to use for Stock-like/Futures-like behavior - - ``_commtype``: Final value to use for PERC vs FIXED commissions - - This two are used internally instead of the declared params to enable the - compatibility check described above for the legacy ``CommissionInfo`` - object + ``_stocklike`` 和 ``_commtype`` 在内部使用,而不是直接使用声明参数,以便 + 执行上面描述的旧版 ``CommissionInfo`` 兼容性检查。 ''' @@ -136,13 +101,11 @@ def __init__(self): self._stocklike = self.p.stocklike self._commtype = self.p.commtype - # The intial block checks for the behavior of the original - # CommissionInfo in which the commission scheme (perc/fixed) was - # determined by parameter "margin" evaluating to False/True - # If the parameter "commtype" is None, this behavior is emulated - # else, the parameter values are used + # 初始代码块检查原始 CommissionInfo 的行为:commission scheme + # (perc/fixed) 由参数 "margin" 的 False/True 判定。如果参数 + # "commtype" 为 None,则模拟该行为;否则使用参数值 - if self._commtype is None: # original CommissionInfo behavior applies + if self._commtype is None: # 使用原始 CommissionInfo 行为 if self.p.margin: self._stocklike = False self._commtype = self.COMM_FIXED @@ -151,7 +114,7 @@ def __init__(self): self._commtype = self.COMM_PERC if not self._stocklike and not self.p.margin: - self.p.margin = 1.0 # avoid having None/0 + self.p.margin = 1.0 # 避免 None/0 if self._commtype == self.COMM_PERC and not self.p.percabs: self.p.commission /= 100.0 @@ -167,14 +130,17 @@ def stocklike(self): return self._stocklike def get_margin(self, price): - '''Returns the actual margin/guarantees needed for a single item of the - asset at the given price. The default implementation has this policy: + '''返回给定 price 下单个 asset 实际所需的 margin/guarantees。 - - Use param ``margin`` if param ``automargin`` evaluates to ``False`` + Args: + price (float): 用于计算 margin 的 asset price。 - - Use param ``mult`` * ``price`` if ``automargin < 0`` + Returns: + float: 实际所需 margin。默认实现使用以下策略: - - Use param ``automargin`` * ``price`` if ``automargin > 0`` + - 如果 ``automargin`` 为 ``False``,使用参数 ``margin`` + - 如果 ``automargin < 0``,使用 ``mult * price`` + - 如果 ``automargin > 0``,使用 ``automargin * price`` ''' if not self.p.automargin: return self.p.margin @@ -186,34 +152,68 @@ def get_margin(self, price): def get_leverage(self): - '''Returns the level of leverage allowed for this comission scheme''' + '''返回该 commission scheme 允许的 leverage。 + + Returns: + float: 当前 leverage。 + ''' return self.p.leverage def getsize(self, price, cash): - '''Returns the needed size to meet a cash operation at a given price''' + '''返回给定 price 和 cash 下可满足 cash 操作的 size。 + + Args: + price (float): asset price。 + cash (float): 可用 cash。 + + Returns: + int: 可执行 size。 + ''' if not self._stocklike: return int(self.p.leverage * (cash // self.get_margin(price))) return int(self.p.leverage * (cash // price)) def getoperationcost(self, size, price): - '''Returns the needed amount of cash an operation would cost''' + '''返回一次 operation 所需的 cash 数量。 + + Args: + size (int): operation size。 + price (float): operation price。 + + Returns: + float: 所需 cash。 + ''' if not self._stocklike: return abs(size) * self.get_margin(price) return abs(size) * price def getvaluesize(self, size, price): - '''Returns the value of size for given a price. For future-like - objects it is fixed at size * margin''' + '''返回给定 price 下 size 对应的 value。 + + Args: + size (int): position 或 operation size。 + price (float): asset price。 + + Returns: + float: 对应 value。对 future-like 对象,固定为 ``size * margin``。 + ''' if not self._stocklike: return abs(size) * self.get_margin(price) return size * price def getvalue(self, position, price): - '''Returns the value of a position given a price. For future-like - objects it is fixed at size * margin''' + '''返回给定 price 下 position 的 value。 + + Args: + position: 带有 ``size`` 和 ``price`` 属性的 position 对象。 + price (float): 当前 asset price。 + + Returns: + float: position value。对 future-like 对象,固定为 ``size * margin``。 + ''' if not self._stocklike: return abs(position.size) * self.get_margin(price) @@ -221,15 +221,21 @@ def getvalue(self, position, price): if size >= 0: return size * price - # With stocks, a short position is worth more as the price goes down + # 对股票来说,short position 会在 price 下跌时更有价值 value = position.price * size # original value value += (position.price - price) * size # increased value return value def _getcommission(self, size, price, pseudoexec): - '''Calculates the commission of an operation at a given price + '''计算给定 price 下一次 operation 的 commission。 - pseudoexec: if True the operation has not yet been executed + Args: + size (int): operation size。 + price (float): operation price。 + pseudoexec (bool): 如果为 ``True``,表示 operation 尚未实际执行。 + + Returns: + float: commission 金额。 ''' if self._commtype == self.COMM_PERC: return abs(size) * self.p.commission * price @@ -237,30 +243,73 @@ def _getcommission(self, size, price, pseudoexec): return abs(size) * self.p.commission def getcommission(self, size, price): - '''Calculates the commission of an operation at a given price + '''计算给定 price 下一次 operation 的 commission。 + + Args: + size (int): operation size。 + price (float): operation price。 + + Returns: + float: commission 金额。 ''' return self._getcommission(size, price, pseudoexec=True) def confirmexec(self, size, price): + '''确认实际执行后的 commission。 + + Args: + size (int): executed size。 + price (float): executed price。 + + Returns: + float: 实际执行 commission。 + ''' return self._getcommission(size, price, pseudoexec=False) def profitandloss(self, size, price, newprice): - '''Return actual profit and loss a position has''' + '''返回 position 的实际 profit and loss。 + + Args: + size (int): position size。 + price (float): 原始 price。 + newprice (float): 新 price。 + + Returns: + float: profit and loss。 + ''' return size * (newprice - price) * self.p.mult def cashadjust(self, size, price, newprice): - '''Calculates cash adjustment for a given price difference''' + '''根据 price 差值计算 cash adjustment。 + + Args: + size (int): position size。 + price (float): 原始 price。 + newprice (float): 新 price。 + + Returns: + float: cash adjustment。stock-like 对象返回 ``0.0``。 + ''' if not self._stocklike: return size * (newprice - price) * self.p.mult return 0.0 def get_credit_interest(self, data, pos, dt): - '''Calculates the credit due for short selling or product specific''' + '''计算 short selling 或特定产品产生的 credit interest。 + + Args: + data: 产生 interest 的 data feed。 + pos: 当前 position,需包含 ``size``、``price`` 和 ``datetime``。 + dt (datetime.datetime): 当前 datetime。 + + Returns: + float: 应计 credit interest。 + ''' size, price = pos.size, pos.price if size > 0 and not self.p.interest_long: - return 0.0 # long positions not charged + return 0.0 # long positions 不收费 dt0 = dt.date() dt1 = pos.datetime.date() @@ -273,54 +322,47 @@ def get_credit_interest(self, data, pos, dt): def _get_credit_interest(self, data, size, price, days, dt0, dt1): ''' - This method returns the cost in terms of credit interest charged by - the broker. - - In the case of ``size > 0`` this method will only be called if the - parameter to the class ``interest_long`` is ``True`` - - The formulat for the calculation of the credit interest rate is: + 返回 broker 收取的 credit interest 成本。 - The formula: ``days * price * abs(size) * (interest / 365)`` + 当 ``size > 0`` 时,只有类参数 ``interest_long`` 为 ``True`` 才会调用 + 该方法。 + credit interest rate 的计算公式为: - Params: - - ``data``: data feed for which interest is charged + ``days * price * abs(size) * (interest / 365)`` - - ``size``: current position size. > 0 for long positions and < 0 for - short positions (this parameter will not be ``0``) - - ``price``: current position price + Args: + data: 收取 interest 的 data feed。 + size (int): 当前 position size。> 0 表示 long position,< 0 表示 + short position(该参数不会为 ``0``)。 + price (float): 当前 position price。 + days (int): 距上次 credit 计算经过的天数,即 ``(dt0 - dt1).days``。 + dt0 (datetime.datetime): 当前 datetime。 + dt1 (datetime.datetime): 上次计算的 datetime。 - - ``days``: number of days elapsed since last credit calculation - (this is (dt0 - dt1).days) + Returns: + float: credit interest 成本。 - - ``dt0``: (datetime.datetime) current datetime - - - ``dt1``: (datetime.datetime) datetime of previous calculation - - ``dt0`` and ``dt1`` are not used in the default implementation and are - provided as extra input for overridden methods + ``dt0`` 和 ``dt1`` 在默认实现中未使用,只是作为额外输入提供给覆盖方法。 ''' return days * self._creditrate * abs(size) * price class CommissionInfo(CommInfoBase): - '''Base Class for the actual Commission Schemes. - - CommInfoBase was created to keep suppor for the original, incomplete, - support provided by *backtrader*. New commission schemes derive from this - class which subclasses ``CommInfoBase``. + '''实际 Commission Schemes 的基类,用于兼容旧版 commission 行为。 - The default value of ``percabs`` is also changed to ``True`` + ``CommInfoBase`` 用于保留 *backtrader* 原始但不完整的支持。新的 commission + schemes 从该类派生,而该类继承 ``CommInfoBase``。 - Params: + ``percabs`` 的默认值也改为 ``True``。 - - ``percabs`` (def: True): when ``commtype`` is set to COMM_PERC, whether - the parameter ``commission`` has to be understood as XX% or 0.XX + Args: + percabs (bool): 当 ``commtype`` 为 COMM_PERC 时,表示参数 + ``commission`` 应按 XX% 还是 0.XX 理解。``True`` 表示 0.XX, + ``False`` 表示 XX%。 - If this param is True: 0.XX - If this param is False: XX% + 旧版 ``CommissionInfo`` 将 0.xx 作为百分比输入。 ''' params = ( diff --git a/backtrader/commissions/__init__.py b/backtrader/commissions/__init__.py index 209627eb6..cb2dfa6e5 100644 --- a/backtrader/commissions/__init__.py +++ b/backtrader/commissions/__init__.py @@ -25,40 +25,47 @@ class CommInfo(CommInfoBase): - pass # clone of CommissionInfo but with xx% instead of 0.xx + '''CommissionInfo 的兼容变体,使用 ``xx%`` 表示百分比而不是 ``0.xx``。''' + pass class CommInfo_Futures(CommInfoBase): + '''futures-like commission scheme 的基类。''' params = ( ('stocklike', False), ) class CommInfo_Futures_Perc(CommInfo_Futures): + '''按百分比计算的 futures-like commission scheme。''' params = ( ('commtype', CommInfoBase.COMM_PERC), ) class CommInfo_Futures_Fixed(CommInfo_Futures): + '''按固定金额计算的 futures-like commission scheme。''' params = ( ('commtype', CommInfoBase.COMM_FIXED), ) class CommInfo_Stocks(CommInfoBase): + '''stock-like commission scheme 的基类。''' params = ( ('stocklike', True), ) class CommInfo_Stocks_Perc(CommInfo_Stocks): + '''按百分比计算的 stock-like commission scheme。''' params = ( ('commtype', CommInfoBase.COMM_PERC), ) class CommInfo_Stocks_Fixed(CommInfo_Stocks): + '''按固定金额计算的 stock-like commission scheme。''' params = ( ('commtype', CommInfoBase.COMM_FIXED), ) diff --git a/backtrader/dataseries.py b/backtrader/dataseries.py index f35c170f4..43e283c1c 100644 --- a/backtrader/dataseries.py +++ b/backtrader/dataseries.py @@ -31,6 +31,8 @@ class TimeFrame(object): + '''timeframe 枚举容器,用于统一表示 data 的时间粒度。''' + (Ticks, MicroSeconds, Seconds, Minutes, Days, Weeks, Months, Years, NoTimeFrame) = range(1, 10) @@ -41,11 +43,12 @@ class TimeFrame(object): @classmethod def getname(cls, tframe, compression=None): + '''返回 timeframe 名称,并在 compression 为 1 时使用单数形式。''' tname = cls.Names[tframe] if compression > 1 or tname == cls.Names[-1]: - return tname # for plural or 'NoTimeFrame' return plain entry + return tname # 复数或 'NoTimeFrame' 直接返回原条目 - # return singular if compression is 1 + # compression 为 1 时返回单数形式 return cls.Names[tframe][:-1] @classmethod @@ -58,6 +61,8 @@ def TName(cls, tframe): class DataSeries(LineSeries): + '''data series 的基类,用于提供 OHLCV line、timeframe 和 writer 输出信息。''' + plotinfo = dict(plot=True, plotind=True, plotylimited=True) _name = '' @@ -90,12 +95,12 @@ def getwritervalues(self): for i in range(len(self.LineOrder), self.lines.size()): values.append(self.lines[i][0]) else: - values.extend([''] * self.lines.size()) # no values yet + values.extend([''] * self.lines.size()) # 尚无 values return values def getwriterinfo(self): - # returns dictionary with information + # 返回包含 data 描述信息的 dictionary info = OrderedDict() info['Name'] = self._name info['Timeframe'] = TimeFrame.TName(self._timeframe) @@ -105,25 +110,50 @@ def getwriterinfo(self): class OHLC(DataSeries): + '''OHLCV data series 的基类,用于定义标准价格与成交量 line。''' + lines = ('close', 'low', 'high', 'open', 'volume', 'openinterest',) class OHLCDateTime(OHLC): + '''带 datetime line 的 OHLC data series 基类。''' + lines = (('datetime'),) class SimpleFilterWrapper(object): - '''Wrapper for filters added via .addfilter to turn them - into processors. - - Filters are callables which - - - Take a ``data`` as an argument - - Return False if the current bar has not triggered the filter - - Return True if the current bar must be filtered - - The wrapper takes the return value and executes the bar removal - if needed be + '''filter wrapper,用于把通过 ``.addfilter`` 添加的 filter 转成 processor。 + + filter 是 callable,约定如下: + + - 接收 ``data`` 作为参数 + - 当前 bar 未触发 filter 时返回 ``False`` + - 当前 bar 必须被过滤时返回 ``True`` + + wrapper 会读取返回值,并在需要时执行 bar removal。 + + Args: + data: 绑定的 data feed。 + ffilter: filter callable 或 filter class。 + *args: 传给 filter 的位置参数。 + **kwargs: 传给 filter 的关键字参数。 + + Returns: + SimpleFilterWrapper: 可被 data feed 调用的 filter wrapper。 + + --- + 交互示例: + >>> class Data: + ... def __init__(self): + ... self.backwards_called = False + ... def backwards(self): + ... self.backwards_called = True + >>> data = Data() + >>> wrapper = SimpleFilterWrapper(data, lambda data: True) + >>> wrapper(data) + True + >>> data.backwards_called + True ''' def __init__(self, data, ffilter, *args, **kwargs): if inspect.isclass(ffilter): @@ -144,20 +174,25 @@ def __call__(self, data): class _Bar(AutoOrderedDict): - ''' - This class is a placeholder for the values of the standard lines of a - DataBase class (from OHLCDateTime) + '''DataBase 标准 line 值的占位容器。 + + 该类保存 ``OHLCDateTime`` 标准 line 的当前 bar 值,并继承 ``AutoOrderedDict``, + 以便按 iterable 返回 values,同时支持按属性访问 key。 - It inherits from AutoOrderedDict to be able to easily return the values as - an iterable and address the keys as attributes + 定义顺序很重要,必须与 ``DataBase`` 中继承自 ``OHLCDateTime`` 的 line 定义一致。 - Order of definition is important and must match that of the lines - definition in DataBase (which directly inherits from OHLCDateTime) + --- + 交互示例: + >>> bar = _Bar() + >>> bar.isopen() + False + >>> bar.volume + 0.0 ''' replaying = False - # Without - 1 ... converting back to time will not work - # Need another -1 to support timezones which may move the time forward + # 如果不减 1,转换回 time 会失败。 + # 额外再减 1,用于支持可能把 time 向前移动的 timezone。 MAXDATE = date2num(_datetime.datetime.max) - 2 def __init__(self, maxdate=False): @@ -165,8 +200,8 @@ def __init__(self, maxdate=False): self.bstart(maxdate=maxdate) def bstart(self, maxdate=False): - '''Initializes a bar to the default not-updated vaues''' - # Order is important: defined in DataSeries/OHLC/OHLCDateTime + '''将 bar 初始化为默认的未更新值。''' + # 顺序很重要:由 DataSeries/OHLC/OHLCDateTime 定义 self.close = float('NaN') self.low = float('inf') self.high = float('-inf') @@ -176,20 +211,22 @@ def bstart(self, maxdate=False): self.datetime = self.MAXDATE if maxdate else None def isopen(self): - '''Returns if a bar has already been updated + '''返回 bar 是否已经被更新。 - Uses the fact that NaN is the value which is not equal to itself - and ``open`` is initialized to NaN + 该方法利用 NaN 不等于自身的事实;``open`` 初始化为 NaN。 ''' o = self.open - return o == o # False if NaN, True in other cases + return o == o # NaN 时为 False,其它情况为 True def bupdate(self, data, reopen=False): - '''Updates a bar with the values from data + '''使用 data 中的值更新当前 bar。 - Returns True if the update was the 1st on a bar (just opened) + Args: + data: 提供 OHLCV 当前值的 data feed。 + reopen: 是否先重新初始化 bar。 - Returns False otherwise + Returns: + bool: 如果这是该 bar 的首次更新(刚打开)则返回 ``True``,否则返回 ``False``。 ''' if reopen: self.bstart() @@ -206,6 +243,6 @@ def bupdate(self, data, reopen=False): o = self.open if reopen or not o == o: self.open = data.open[0] - return True # just opened the bar + return True # 刚打开 bar return False diff --git a/backtrader/errors.py b/backtrader/errors.py index 5f3dcdb33..8ac485cf5 100644 --- a/backtrader/errors.py +++ b/backtrader/errors.py @@ -26,26 +26,38 @@ class BacktraderError(Exception): - '''Base exception for all other exceptions''' + '''backtrader 异常的基类,用于统一承载框架内错误。''' pass class StrategySkipError(BacktraderError): - '''Requests the platform to skip this strategy for backtesting. To be - raised during the initialization (``__init__``) phase of the instance''' + '''请求平台在回测中跳过当前 strategy。 + + 该异常应在 strategy 实例初始化(``__init__``)阶段抛出。 + ''' pass class ModuleImportError(BacktraderError): - '''Raised if a class requests a module to be present to work and it cannot - be imported''' + '''依赖模块缺失时抛出的异常。 + + 当某个类需要指定 module 才能工作,但该 module 无法 import 时使用。 + + Args: + message: 给调用方展示的错误信息。 + *args: 与缺失 module 相关的附加上下文。 + ''' def __init__(self, message, *args): super(ModuleImportError, self).__init__(message) self.args = args class FromModuleImportError(ModuleImportError): - '''Raised if a class requests a module to be present to work and it cannot - be imported''' + '''``from module import name`` 形式依赖缺失时抛出的异常。 + + Args: + message: 给调用方展示的错误信息。 + *args: 与缺失对象相关的附加上下文。 + ''' def __init__(self, message, *args): super(FromModuleImportError, self).__init__(message, *args) diff --git a/backtrader/feed.py b/backtrader/feed.py index 84c6b2b89..3bc5e0537 100644 --- a/backtrader/feed.py +++ b/backtrader/feed.py @@ -39,13 +39,13 @@ class MetaAbstractDataBase(dataseries.OHLCDateTime.__class__): + '''DataBase metaclass 的基类,用于登记 data feed 子类并完成初始化挂接。''' + _indcol = dict() def __init__(cls, name, bases, dct): - ''' - Class has already been created ... register subclasses - ''' - # Initialize the class + '''类已创建完成,登记 data feed 子类。''' + # 初始化类 super(MetaAbstractDataBase, cls).__init__(name, bases, dct) if not cls.aliased and \ @@ -56,10 +56,10 @@ def dopreinit(cls, _obj, *args, **kwargs): _obj, args, kwargs = \ super(MetaAbstractDataBase, cls).dopreinit(_obj, *args, **kwargs) - # Find the owner and store it + # 查找 owner 并保存 _obj._feed = metabase.findowner(_obj, FeedBase) - _obj.notifs = collections.deque() # store notifications for cerebro + _obj.notifs = collections.deque() # 保存发给 cerebro 的 notifications _obj._dataname = _obj.p.dataname _obj._name = '' @@ -69,7 +69,7 @@ def dopostinit(cls, _obj, *args, **kwargs): _obj, args, kwargs = \ super(MetaAbstractDataBase, cls).dopostinit(_obj, *args, **kwargs) - # Either set by subclass or the parameter or use the dataname (ticker) + # 使用子类设置的名称、参数名称,或 dataname(ticker) _obj._name = _obj._name or _obj.p.name if not _obj._name and isinstance(_obj.p.dataname, string_types): _obj._name = _obj.p.dataname @@ -86,25 +86,23 @@ def dopostinit(cls, _obj, *args, **kwargs): _obj.p.sessionend = _obj.p.sessionend.time() elif _obj.p.sessionend is None: - # remove 9 to avoid precision rounding errors + # 减少 9 微秒以避免 precision rounding errors _obj.p.sessionend = datetime.time(23, 59, 59, 999990) if isinstance(_obj.p.fromdate, datetime.date): - # push it to the end of the day, or else intraday - # values before the end of the day would be gone + # 推到当天 sessionstart,否则日内数据在当天结束前的值会被过滤掉 if not hasattr(_obj.p.fromdate, 'hour'): _obj.p.fromdate = datetime.datetime.combine( _obj.p.fromdate, _obj.p.sessionstart) if isinstance(_obj.p.todate, datetime.date): - # push it to the end of the day, or else intraday - # values before the end of the day would be gone + # 推到当天 sessionend,否则日内数据在当天结束前的值会被过滤掉 if not hasattr(_obj.p.todate, 'hour'): _obj.p.todate = datetime.datetime.combine( _obj.p.todate, _obj.p.sessionend) - _obj._barstack = collections.deque() # for filter operations - _obj._barstash = collections.deque() # for filter operations + _obj._barstack = collections.deque() # 用于 filter operations + _obj._barstash = collections.deque() # 用于 filter operations _obj._filters = list() _obj._ffilters = list() @@ -121,6 +119,7 @@ def dopostinit(cls, _obj, *args, **kwargs): class AbstractDataBase(with_metaclass(MetaAbstractDataBase, dataseries.OHLCDateTime)): + '''data feed 的抽象基类,用于管理时间过滤、通知、filter 和 bar 加载流程。''' params = ( ('dataname', None), @@ -134,7 +133,7 @@ class AbstractDataBase(with_metaclass(MetaAbstractDataBase, ('filters', []), ('tz', None), ('tzinput', None), - ('qcheck', 0.0), # timeout in seconds (float) to check for events + ('qcheck', 0.0), # 检查 event 的超时时间,单位为秒(float) ('calendar', None), ) @@ -158,25 +157,24 @@ def _getstatusname(cls, status): _tmoffset = datetime.timedelta() - # Set to non 0 if resampling/replaying + # resampling/replaying 时设为非 0 resampling = 0 replaying = 0 _started = False def _start_finish(self): - # A live feed (for example) may have learnt something about the - # timezones after the start and that's why the date/time related - # parameters are converted at this late stage - # Get the output timezone (if any) + # live feed 可能在 start 后才知道 timezone 信息,因此 date/time 相关参数 + # 在这个较晚阶段统一转换。 + # 获取输出 timezone(如果有) self._tz = self._gettz() - # Lines have already been create, set the tz + # Lines 已经创建,设置 tz self.lines.datetime._settz(self._tz) - # This should probably be also called from an override-able method + # 这也许也应该由一个可覆盖方法调用 self._tzinput = bt.utils.date.Localizer(self._gettzinput()) - # Convert user input times to the output timezone (or min/max) + # 将用户输入时间转换到输出 timezone(或 min/max) if self.p.fromdate is None: self.fromdate = float('-inf') else: @@ -187,7 +185,7 @@ def _start_finish(self): else: self.todate = self.date2num(self.p.todate) - # FIXME: These two are never used and could be removed + # FIXME: 这两个值从未使用,可以移除 self.sessionstart = time2num(self.p.sessionstart) self.sessionend = time2num(self.p.sessionend) @@ -209,7 +207,7 @@ def _timeoffset(self): return self._tmoffset def _getnexteos(self): - '''Returns the next eos using a trading calendar if available''' + '''使用 trading calendar 返回下一个 end-of-session(如果可用)。''' if self._clone: return self.data._getnexteos() @@ -220,27 +218,26 @@ def _getnexteos(self): dtime = num2date(dt) if self._calendar is None: nexteos = datetime.datetime.combine(dtime, self.p.sessionend) - nextdteos = self.date2num(nexteos) # locl'ed -> utc-like + nextdteos = self.date2num(nexteos) # localized -> utc-like nexteos = num2date(nextdteos) # utc while dtime > nexteos: - nexteos += datetime.timedelta(days=1) # already utc-like + nexteos += datetime.timedelta(days=1) # 已经是 utc-like nextdteos = date2num(nexteos) # -> utc-like else: - # returns times in utc + # 返回 utc 时间 _, nexteos = self._calendar.schedule(dtime, self._tz) nextdteos = date2num(nexteos) # nextos is already utc return nexteos, nextdteos def _gettzinput(self): - '''Can be overriden by classes to return a timezone for input''' + '''供子类覆盖,用于返回输入 timezone。''' return tzparse(self.p.tzinput) def _gettz(self): - '''To be overriden by subclasses which may auto-calculate the - timezone''' + '''供可自动计算 timezone 的子类覆盖。''' return tzparse(self.p.tz) def date2num(self, dt): @@ -256,36 +253,36 @@ def num2date(self, dt=None, tz=None, naive=True): return num2date(dt, tz or self._tz, naive) def haslivedata(self): - return False # must be overriden for those that can + return False # 支持 live data 的子类必须覆盖 def do_qcheck(self, onoff, qlapse): - # if onoff is True the data will wait p.qcheck for incoming live data - # on its queue. + # onoff 为 True 时,data 会在队列上等待 p.qcheck,以接收 live data。 qwait = self.p.qcheck if onoff else 0.0 qwait = max(0.0, qwait - qlapse) self._qcheck = qwait def islive(self): - '''If this returns True, ``Cerebro`` will deactivate ``preload`` and - ``runonce`` because a live data source must be fetched tick by tick (or - bar by bar)''' + '''返回该 data feed 是否为 live feed。 + + 如果返回 ``True``,``Cerebro`` 会停用 ``preload`` 和 ``runonce``,因为 live + data source 必须逐 tick(或逐 bar)获取。 + ''' return False def put_notification(self, status, *args, **kwargs): - '''Add arguments to notification queue''' + '''向 notification queue 添加状态通知。''' if self._laststatus != status: self.notifs.append((status, args, kwargs)) self._laststatus = status def get_notifications(self): - '''Return the pending "store" notifications''' - # The background thread could keep on adding notifications. The None - # mark allows to identify which is the last notification to deliver - self.notifs.append(None) # put a mark + '''返回待处理的 data notification。''' + # 后台线程可能持续添加 notification。None 标记用于识别本轮最后一个待交付项。 + self.notifs.append(None) # 放置标记 notifs = list() while True: notif = self.notifs.popleft() - if notif is None: # mark is reached + if notif is None: # 到达标记 break notifs.append(notif) @@ -317,7 +314,7 @@ def copyas(self, _dataname, **kwargs): return d def setenvironment(self, env): - '''Keep a reference to the environment''' + '''保存 environment 引用。''' self._env = env def getenvironment(self): @@ -339,16 +336,14 @@ def addfilter(self, p, *args, **kwargs): self._filters.append((p, args, kwargs)) def compensate(self, other): - '''Call it to let the broker know that actions on this asset will - compensate open positions in another''' + '''告知 broker:该资产上的操作会抵消另一个资产的 open position。''' self._compensate = other def _tick_nullify(self): - # These are the updating prices in case the new bar is "updated" - # and the length doesn't change like if a replay is happening or - # a real-time data feed is in use and 1 minutes bars are being - # constructed with 5 seconds updates + # 这些是 new bar 被“更新”时使用的更新价格。 + # 例如 replay 正在发生,或 real-time data feed 用 5 秒更新构造 1 分钟 bar, + # 此时长度不会变化。 for lalias in self.getlinealiases(): if lalias != 'datetime': setattr(self, 'tick_' + lalias, None) @@ -356,7 +351,7 @@ def _tick_nullify(self): self.tick_last = None def _tick_fill(self, force=False): - # If nothing filled the tick_xxx attributes, the bar is the tick + # 如果没有填充 tick_xxx 属性,则当前 bar 本身就是 tick alias0 = self._getlinealias(0) if force or getattr(self, 'tick_' + alias0, None) is None: for lalias in self.getlinealiases(): @@ -368,21 +363,20 @@ def _tick_fill(self, force=False): def advance_peek(self): if len(self) < self.buflen(): - return self.lines.datetime[1] # return the future + return self.lines.datetime[1] # 返回未来时间 - return float('inf') # max date else + return float('inf') # 否则返回最大日期 def advance(self, size=1, datamaster=None, ticks=True): if ticks: self._tick_nullify() - # Need intercepting this call to support datas with - # different lengths (timeframes) + # 需要拦截该调用,以支持不同长度(timeframes)的 data self.lines.advance(size) if datamaster is not None: if len(self) > self.buflen(): - # if no bar can be delivered, fill with an empty bar + # 没有 bar 可交付时,填充一个 empty bar self.rewind() self.lines.forward() return @@ -393,7 +387,7 @@ def advance(self, size=1, datamaster=None, ticks=True): if ticks: self._tick_fill() elif len(self) < self.buflen(): - # a resampler may have advance us past the last point + # resampler 可能已经把当前位置 advance 到最后一个点之后 if ticks: self._tick_fill() @@ -403,25 +397,25 @@ def next(self, datamaster=None, ticks=True): if ticks: self._tick_nullify() - # not preloaded - request next bar + # 未 preload,直接请求下一根 bar ret = self.load() if not ret: - # if load cannot produce bars - forward the result + # 如果 load 无法生成 bar,直接转发结果 return ret if datamaster is None: - # bar is there and no master ... return load's result + # bar 已存在且无 master,返回 load 的结果 if ticks: self._tick_fill() return ret else: self.advance(ticks=ticks) - # a bar is "loaded" or was preloaded - index has been moved to it + # bar 已“loaded”或已 preload,索引已移动到该位置 if datamaster is not None: - # there is a time reference to check against + # 存在时间参考,需要对齐检查 if self.lines.datetime[0] > datamaster.lines.datetime[0]: - # can't deliver new bar, too early, go back + # 时间过早,无法交付 new bar,回退 self.rewind() return False else: @@ -432,7 +426,7 @@ def next(self, datamaster=None, ticks=True): if ticks: self._tick_fill() - # tell the world there is a bar (either the new or the previous + # 告知外部已有 bar(新的或上一根) return True def preload(self): @@ -443,7 +437,7 @@ def preload(self): self.home() def _last(self, datamaster=None): - # Last chance for filters to deliver something + # filters 最后一次交付内容的机会 ret = 0 for ff, fargs, fkwargs in self._ffilters: ret += ff.last(self, *fargs, **fkwargs) @@ -453,7 +447,7 @@ def _last(self, datamaster=None): doticks = True while self._fromstack(forward=True): - # consume bar(s) produced by "last"s - adding room + # 消费由 "last" 产生的 bar,并腾出空间 pass if doticks: @@ -470,7 +464,7 @@ def _check(self, forcedata=None): def load(self): while True: - # move data pointer forward for new bar + # 为 new bar 将 data pointer 向前移动 self.forward() if self._fromstack(): # bar is available @@ -478,44 +472,40 @@ def load(self): if not self._fromstack(stash=True): _loadret = self._load() - if not _loadret: # no bar use force to make sure in exactbars - # the pointer is undone this covers especially (but not - # uniquely) the case in which the last bar has been seen - # and a backwards would ruin pointer accounting in the - # "stop" method of the strategy - self.backwards(force=True) # undo data pointer - - # return the actual returned value which may be None to - # signal no bar is available, but the data feed is not - # done. False means game over + if not _loadret: # 无 bar 时使用 force,确保 exactbars 场景正确 + # 撤销 pointer,尤其覆盖已经看到最后一根 bar 的情况。 + # 此时普通 backwards 会破坏 strategy "stop" 方法中的 pointer 记账。 + self.backwards(force=True) # 撤销 data pointer + + # 返回实际返回值:None 表示当前无 bar 可用但 data feed 尚未结束; + # False 表示彻底结束。 return _loadret - # Get a reference to current loaded time + # 获取当前 loaded time 的引用 dt = self.lines.datetime[0] - # A bar has been loaded, adapt the time + # bar 已加载,适配时间 if self._tzinput: - # Input has been converted at face value but it's not UTC in - # the input stream - dtime = num2date(dt) # get it in a naive datetime - # localize it + # input stream 中的时间按表面值转换,但它并不是 UTC + dtime = num2date(dt) # 转为 naive datetime + # localize dtime = self._tzinput.localize(dtime) # pytz compatible-ized self.lines.datetime[0] = dt = date2num(dtime) # keep UTC val - # Check standard date from/to filters + # 检查标准 from/to date filter if dt < self.fromdate: - # discard loaded bar and carry on + # 丢弃 loaded bar 并继续 self.backwards() continue if dt > self.todate: - # discard loaded bar and break out + # 丢弃 loaded bar 并退出 self.backwards(force=True) break - # Pass through filters + # 通过 filters retff = False for ff, fargs, fkwargs in self._filters: - # previous filter may have put things onto the stack + # 前一个 filter 可能已把内容放入 stack if self._barstack: for i in range(len(self._barstack)): self._fromstack(forward=True) @@ -523,32 +513,35 @@ def load(self): else: retff = ff(self, *fargs, **fkwargs) - if retff: # bar removed from systemn - break # out of the inner loop + if retff: # bar 已从系统中移除 + break # 跳出内层 loop - if retff: # bar removed from system - loop to get new bar - continue # in the greater loop + if retff: # bar 已从系统中移除,继续外层 loop 获取 new bar + continue - # Checks let the bar through ... notify it + # checks 允许 bar 通过,通知调用方 return True - # Out of the loop ... no more bars or past todate + # 跳出 loop,表示无更多 bar 或已超过 todate return False def _load(self): return False def _add2stack(self, bar, stash=False): - '''Saves given bar (list of values) to the stack for later retrieval''' + '''将给定 bar(value list)保存到 stack,供之后取回。''' if not stash: self._barstack.append(bar) else: self._barstash.append(bar) def _save2stack(self, erase=False, force=False, stash=False): - '''Saves current bar to the bar stack for later retrieval + '''将当前 bar 保存到 bar stack,供之后取回。 - Parameter ``erase`` determines removal from the data stream + Args: + erase: 是否从 data stream 中移除当前 bar。 + force: 移除时是否强制回退 pointer。 + stash: 是否保存到 stash stack。 ''' bar = [line[0] for line in self.itersize()] if not stash: @@ -556,13 +549,16 @@ def _save2stack(self, erase=False, force=False, stash=False): else: self._barstash.append(bar) - if erase: # remove bar if requested + if erase: # 按请求移除 bar self.backwards(force=force) def _updatebar(self, bar, forward=False, ago=0): - '''Load a value from the stack onto the lines to form the new bar + '''把 stack 中的 value 加载到 lines,以形成 new bar。 - Returns True if values are present, False otherwise + Args: + bar: 要写入 line 的 value list。 + forward: 写入前是否先 forward。 + ago: 写入位置相对当前 bar 的偏移。 ''' if forward: self.forward() @@ -571,9 +567,14 @@ def _updatebar(self, bar, forward=False, ago=0): line[0 + ago] = val def _fromstack(self, forward=False, stash=False): - '''Load a value from the stack onto the lines to form the new bar + '''从 stack 取出 value 并加载到 lines,以形成 new bar。 + + Args: + forward: 写入前是否先 forward。 + stash: 是否从 stash stack 读取。 - Returns True if values are present, False otherwise + Returns: + bool: stack 中有 value 并成功加载时返回 ``True``,否则返回 ``False``。 ''' coll = self._barstack if not stash else self._barstash @@ -597,10 +598,13 @@ def replay(self, **kwargs): class DataBase(AbstractDataBase): + '''DataBase 的基类,用于作为所有具体 data feed 的共同父类。''' pass class FeedBase(with_metaclass(metabase.MetaParams, object)): + '''Feed 的基类,用于管理多个 data feed 实例的创建、启动和停止。''' + params = () + DataBase.params._gettuple() def __init__(self): @@ -635,8 +639,10 @@ def _getdata(self, dataname, **kwargs): class MetaCSVDataBase(DataBase.__class__): + '''CSV data feed metaclass 的基类,用于从文件名推导默认 data 名称。''' + def dopostinit(cls, _obj, *args, **kwargs): - # Before going to the base class to make sure it overrides the default + # 先于 base class 处理,确保覆盖默认值 if not _obj.p.name and not _obj._name: _obj._name, _ = os.path.splitext(os.path.basename(_obj.p.dataname)) @@ -647,18 +653,22 @@ def dopostinit(cls, _obj, *args, **kwargs): class CSVDataBase(with_metaclass(MetaCSVDataBase, DataBase)): - ''' - Base class for classes implementing CSV DataFeeds + '''CSV DataFeed 的基类。 + + 该类负责打开文件、逐行读取并按 separator 切分 token。 - The class takes care of opening the file, reading the lines and - tokenizing them. + 子类通常只需要覆盖: - Subclasses do only need to override: + - ``_loadline(tokens)`` - - _loadline(tokens) + ``_loadline`` 的返回值(``True``/``False``)会作为本基类覆盖的 ``_load`` 返回值。 - The return value of ``_loadline`` (True/False) will be the return value - of ``_load`` which has been overriden by this base class + Args: + headers: CSV 是否包含表头;默认 ``True`` 时启动阶段跳过第一行。 + separator: CSV 字段分隔符。 + + Returns: + CSVDataBase: 可由具体 CSV feed 继承的 data feed 基类。 ''' f = None @@ -671,11 +681,11 @@ def start(self): if hasattr(self.p.dataname, 'readline'): self.f = self.p.dataname else: - # Let an exception propagate to let the caller know + # 允许异常向上传播,让调用方知道打开失败 self.f = io.open(self.p.dataname, 'r') if self.p.headers: - self.f.readline() # skip the headers + self.f.readline() # 跳过 header self.separator = self.p.separator @@ -692,7 +702,7 @@ def preload(self): self._last() self.home() - # preloaded - no need to keep the object around - breaks multip in 3.x + # 已 preload,无需继续持有文件对象;在 3.x 中会破坏 multiprocessing self.f.close() self.f = None @@ -700,7 +710,7 @@ def _load(self): if self.f is None: return False - # Let an exception propagate to let the caller know + # 允许异常向上传播,让调用方知道读取失败 line = self.f.readline() if not line: @@ -714,7 +724,7 @@ def _getnextline(self): if self.f is None: return None - # Let an exception propagate to let the caller know + # 允许异常向上传播,让调用方知道读取失败 line = self.f.readline() if not line: @@ -726,6 +736,8 @@ def _getnextline(self): class CSVFeedBase(FeedBase): + '''CSV Feed 的基类,用于按 basepath 创建 CSVDataBase 子类实例。''' + params = (('basepath', ''),) + CSVDataBase.params._gettuple() def _getdata(self, dataname, **kwargs): @@ -734,13 +746,15 @@ def _getdata(self, dataname, **kwargs): class DataClone(AbstractDataBase): + '''data clone 的基类,用于复用另一个 data feed 的 line 和时间范围信息。''' + _clone = True def __init__(self): self.data = self.p.dataname self._dataname = self.data._dataname - # Copy date/session parameters + # 复制 date/session 参数 self.p.fromdate = self.p.fromdate self.p.todate = self.p.todate self.p.sessionstart = self.data.p.sessionstart @@ -750,19 +764,19 @@ def __init__(self): self.p.compression = self.data.p.compression def _start(self): - # redefine to copy data bits from guest data + # 重新定义,用于从 guest data 复制 data bits self.start() - # Copy tz infos + # 复制 tz 信息 self._tz = self.data._tz self.lines.datetime._settz(self._tz) self._calendar = self.data._calendar - # input has already been converted by guest data - self._tzinput = None # no need to further converr + # input 已由 guest data 转换 + self._tzinput = None # 无需进一步转换 - # Copy dates/session infos + # 复制 dates/session 信息 self.fromdate = self.data.fromdate self.todate = self.data.todate @@ -778,15 +792,14 @@ def start(self): def preload(self): self._preloading = True super(DataClone, self).preload() - self.data.home() # preloading data was pushed forward + self.data.home() # preload 时 data 已被向前推进 self._preloading = False def _load(self): - # assumption: the data is in the system - # simply copy the lines + # 假设 data 已在系统中,直接复制 lines if self._preloading: - # data is preloaded, we are preloading too, can move - # forward until have full bar or data source is exhausted + # data 已 preload,本 clone 也在 preload;可持续向前移动, + # 直到得到完整 bar 或 data source 耗尽。 self.data.advance() if len(self.data) > self.data.buflen(): return False @@ -796,9 +809,9 @@ def _load(self): return True - # Not preloading + # 非 preload 模式 if not (len(self.data) > self._dlen): - # Data not beyond last seen bar + # Data 尚未超过上次看到的 bar return False self._dlen += 1 diff --git a/backtrader/feeds/__init__.py b/backtrader/feeds/__init__.py index 8054715dc..508caf84d 100644 --- a/backtrader/feeds/__init__.py +++ b/backtrader/feeds/__init__.py @@ -35,17 +35,17 @@ try: from .ibdata import * except ImportError: - pass # The user may not have ibpy installed + pass # 用户可能没有安装 ibpy try: from .vcdata import * except ImportError: - pass # The user may not have something installed + pass # 用户可能没有安装相关可选依赖 try: from .oanda import OandaData except ImportError: - pass # The user may not have something installed + pass # 用户可能没有安装相关可选依赖 from .vchartfile import VChartFile diff --git a/backtrader/feeds/blaze.py b/backtrader/feeds/blaze.py index 5496e50b8..2560e1748 100644 --- a/backtrader/feeds/blaze.py +++ b/backtrader/feeds/blaze.py @@ -27,23 +27,35 @@ class BlazeData(feed.DataBase): ''' - Support for `Blaze `_ ``Data`` objects. + 支持 `Blaze `_ 的 ``Data`` 对象。 - Only numeric indices to columns are supported. + 这里只支持使用数字索引定位列。 - Note: + Args: + dataname: Blaze ``Data`` 对象。 + datetime: datetime 字段的数字列索引,必须存在。 + open: open 字段的数字列索引,传入负数表示不存在。 + high: high 字段的数字列索引,传入负数表示不存在。 + low: low 字段的数字列索引,传入负数表示不存在。 + close: close 字段的数字列索引,传入负数表示不存在。 + volume: volume 字段的数字列索引,传入负数表示不存在。 + openinterest: openinterest 字段的数字列索引,传入负数表示不存在。 - - The ``dataname`` parameter is a blaze ``Data`` object + Returns: + BlazeData: 可加入 Cerebro 的 Blaze 数据源实例。 - - A negative value in any of the parameters for the Data lines - indicates it's not present in the DataFrame - it is + --- + 交互界面使用示范: + + >>> data = BlazeData(dataname=[]) + >>> data.p.datetime + 0 ''' params = ( - # datetime must be present + # datetime 必须存在 ('datetime', 0), - # pass -1 for any of the following to indicate absence + # 下列字段传 -1 表示不存在 ('open', 1), ('high', 2), ('low', 3), @@ -59,7 +71,7 @@ class BlazeData(feed.DataBase): def start(self): super(BlazeData, self).start() - # reset the iterator on each start + # 每次 start 时重置迭代器 self._rows = iter(self.p.dataname) def _load(self): @@ -68,27 +80,27 @@ def _load(self): except StopIteration: return False - # Set the standard datafields - except for datetime + # 设置标准 datafield,datetime 单独处理 for datafield in self.datafields[1:]: - # get the column index + # 获取字段所在的列索引 colidx = getattr(self.params, datafield) if colidx < 0: - # column not present -- skip + # 数据源中没有该列,跳过 continue - # get the line to be set + # 获取要写入的 line line = getattr(self.lines, datafield) line[0] = row[colidx] - # datetime - assumed blaze always serves a native datetime.datetime + # datetime:假定 blaze 总是提供原生 datetime.datetime colidx = getattr(self.params, self.datafields[0]) dt = row[colidx] dtnum = date2num(dt) - # get the line to be set + # 获取要写入的 line line = getattr(self.lines, self.datafields[0]) line[0] = dtnum - # Done ... return + # 当前 bar 加载完成 return True diff --git a/backtrader/feeds/btcsv.py b/backtrader/feeds/btcsv.py index 1d00e0745..98cea6b22 100644 --- a/backtrader/feeds/btcsv.py +++ b/backtrader/feeds/btcsv.py @@ -28,25 +28,29 @@ class BacktraderCSVData(feed.CSVDataBase): - ''' - Parses a self-defined CSV Data used for testing. + '''解析 backtrader 自定义测试 CSV 格式的 data feed。 + + Args: + dataname: 要解析的文件名或 file-like 对象。 - Specific parameters: + Returns: + bool: ``_loadline`` 成功解析一行时返回 ``True``。 - - ``dataname``: The filename to parse or a file-like object + --- + >>> data = BacktraderCSVData(dataname='2006-day-001.txt') ''' def _loadline(self, linetokens): itoken = iter(linetokens) - dttxt = next(itoken) # Format is YYYY-MM-DD - skip char 4 and 7 + dttxt = next(itoken) # 格式为 YYYY-MM-DD,跳过第 4 和第 7 个字符 dt = date(int(dttxt[0:4]), int(dttxt[5:7]), int(dttxt[8:10])) if len(linetokens) == 8: - tmtxt = next(itoken) # Format if present HH:MM:SS, skip 3 and 6 + tmtxt = next(itoken) # 如果存在,格式为 HH:MM:SS,跳过第 3 和第 6 个字符 tm = time(int(tmtxt[0:2]), int(tmtxt[3:5]), int(tmtxt[6:8])) else: - tm = self.p.sessionend # end of the session parameter + tm = self.p.sessionend # session 结束时间参数 self.lines.datetime[0] = date2num(datetime.combine(dt, tm)) self.lines.open[0] = float(next(itoken)) diff --git a/backtrader/feeds/chainer.py b/backtrader/feeds/chainer.py index 9cce29df9..b70378d9f 100644 --- a/backtrader/feeds/chainer.py +++ b/backtrader/feeds/chainer.py @@ -30,13 +30,13 @@ class MetaChainer(bt.DataBase.__class__): def __init__(cls, name, bases, dct): - '''Class has already been created ... register''' - # Initialize the class + '''类已经创建完成,随后进行初始化注册。''' + # 初始化类对象 super(MetaChainer, cls).__init__(name, bases, dct) def donew(cls, *args, **kwargs): - '''Intercept const. to copy timeframe/compression from 1st data''' - # Create the object and set the params in place + '''拦截构造过程,从第一个 data 复制 timeframe/compression。''' + # 创建对象并设置参数 _obj, args, kwargs = super(MetaChainer, cls).donew(*args, **kwargs) if args: @@ -47,11 +47,25 @@ def donew(cls, *args, **kwargs): class Chainer(bt.with_metaclass(MetaChainer, bt.DataBase)): - '''Class that chains datas''' + '''把多个 data feed 按时间顺序串接成一个连续数据源。 + + Args: + *args: 按顺序传入的多个 data feed。前一个耗尽后,会切到后一个;已经输出过 + 的时间不会重复输出。 + + Returns: + Chainer: 可加入 Cerebro 的串接数据源实例。 + + --- + 交互界面使用示范: + + >>> data = Chainer() + >>> data._args + () + ''' def islive(self): - '''Returns ``True`` to notify ``Cerebro`` that preloading and runonce - should be deactivated''' + '''返回 ``True``,通知 ``Cerebro`` 关闭 preload 和 runonce。''' return True def __init__(self, *args): @@ -63,7 +77,7 @@ def start(self): d.setenvironment(self._env) d._start() - # put the references in a separate list to have pops + # 把引用放到单独列表中,便于按顺序 pop self._ds = list(self._args) self._d = self._ds.pop(0) if self._ds else None self._lastdt = datetime.min @@ -77,19 +91,18 @@ def get_notifications(self): return [] if self._d is None else self._d.get_notifications() def _gettz(self): - '''To be overriden by subclasses which may auto-calculate the - timezone''' + '''供子类覆写,用于自动计算 timezone。''' if self._args: return self._args[0]._gettz() return bt.utils.date.Localizer(self.p.tz) def _load(self): while self._d is not None: - if not self._d.next(): # no values from current data source + if not self._d.next(): # 当前数据源已经没有值 self._d = self._ds.pop(0) if self._ds else None continue - # Cannot deliver a date equal or less than an alredy delivered + # 不能输出早于或等于已输出时间的 bar dt = self._d.datetime.datetime() if dt <= self._lastdt: continue @@ -101,5 +114,5 @@ def _load(self): return True - # Out of the loop -> self._d is None, no data feed to return from + # 退出循环表示 self._d 为 None,没有 data feed 可继续返回 return False diff --git a/backtrader/feeds/csvgeneric.py b/backtrader/feeds/csvgeneric.py index cc6a30d0c..0cad5ae5f 100644 --- a/backtrader/feeds/csvgeneric.py +++ b/backtrader/feeds/csvgeneric.py @@ -30,42 +30,34 @@ class GenericCSVData(feed.CSVDataBase): - '''Parses a CSV file according to the order and field presence defined by the - parameters - - Specific parameters (or specific meaning): - - - ``dataname``: The filename to parse or a file-like object - - - The lines parameters (datetime, open, high ...) take numeric values - - A value of -1 indicates absence of that field in the CSV source - - - If ``time`` is present (parameter time >=0) the source contains - separated fields for date and time, which will be combined - - - ``nullvalue`` - - Value that will be used if a value which should be there is missing - (the CSV field is empty) - - - ``dtformat``: Format used to parse the datetime CSV field. See the - python strptime/strftime documentation for the format. - - If a numeric value is specified, it will be interpreted as follows - - - ``1``: The value is a Unix timestamp of type ``int`` representing - the number of seconds since Jan 1st, 1970 - - - ``2``: The value is a Unix timestamp of type ``float`` - - If a **callable** is passed - - - it will accept a string and return a `datetime.datetime` python - instance - - - ``tmformat``: Format used to parse the time CSV field if "present" - (the default for the "time" CSV field is not to be present) + '''按参数定义的字段顺序和字段存在性解析 CSV 文件的 data feed。 + + Args: + dataname: 要解析的文件名或 file-like 对象。 + datetime (int): datetime 字段所在列索引,默认 ``0``。 + time (int): time 字段所在列索引,默认 ``-1``,表示不存在独立 time + 字段。如果 ``time >= 0``,date 和 time 会被合并。 + open (int): open 字段所在列索引,默认 ``1``。 + high (int): high 字段所在列索引,默认 ``2``。 + low (int): low 字段所在列索引,默认 ``3``。 + close (int): close 字段所在列索引,默认 ``4``。 + volume (int): volume 字段所在列索引,默认 ``5``。 + openinterest (int): openinterest 字段所在列索引,默认 ``6``。 + nullvalue: CSV 字段缺失或为空时使用的值,默认 ``NaN``。 + dtformat: 解析 datetime CSV 字段使用的格式,默认 + ``'%Y-%m-%d %H:%M:%S'``。可传入: + + - ``1``: Unix timestamp,``int`` 秒数,自 1970-01-01 起算 + - ``2``: Unix timestamp,``float`` 秒数 + - callable: 接收字符串并返回 ``datetime.datetime`` 实例 + + tmformat: 独立 time CSV 字段存在时使用的解析格式,默认 ``'%H:%M:%S'``。 + + Returns: + bool: ``_loadline`` 成功解析一行时返回 ``True``。 + + --- + >>> data = GenericCSVData(dataname='prices.csv', dtformat='%Y-%m-%d') ''' @@ -97,17 +89,17 @@ def start(self): elif idt == 2: self._dtconvert = lambda x: datetime.utcfromtimestamp(float(x)) - else: # assume callable + else: # 假定为 callable self._dtconvert = self.p.dtformat def _loadline(self, linetokens): - # Datetime needs special treatment + # Datetime 需要特殊处理 dtfield = linetokens[self.p.datetime] if self._dtstr: dtformat = self.p.dtformat if self.p.time >= 0: - # add time value and format if it's in a separate field + # 如果 time 位于独立字段,则追加 time 值和格式 dtfield += 'T' + linetokens[self.p.time] dtformat += 'T' + self.p.tmformat @@ -116,42 +108,42 @@ def _loadline(self, linetokens): dt = self._dtconvert(dtfield) if self.p.timeframe >= TimeFrame.Days: - # check if the expected end of session is larger than parsed + # 检查预期 session end 是否大于解析出的时间 if self._tzinput: - dtin = self._tzinput.localize(dt) # pytz compatible-ized + dtin = self._tzinput.localize(dt) # pytz 兼容化 else: dtin = dt - dtnum = date2num(dtin) # utc'ize + dtnum = date2num(dtin) # 转为 UTC dteos = datetime.combine(dt.date(), self.p.sessionend) - dteosnum = self.date2num(dteos) # utc'ize + dteosnum = self.date2num(dteos) # 转为 UTC if dteosnum > dtnum: self.lines.datetime[0] = dteosnum else: - # Avoid reconversion if already converted dtin == dt + # 如果已经转换且 dtin == dt,避免重复转换 self.l.datetime[0] = date2num(dt) if self._tzinput else dtnum else: self.lines.datetime[0] = date2num(dt) - # The rest of the fields can be done with the same procedure + # 其余字段可用相同流程处理 for linefield in (x for x in self.getlinealiases() if x != 'datetime'): - # Get the index created from the passed params + # 获取由传入 params 创建的索引 csvidx = getattr(self.params, linefield) if csvidx is None or csvidx < 0: - # the field will not be present, assignt the "nullvalue" + # 字段不存在,赋值为 nullvalue csvfield = self.p.nullvalue else: - # get it from the token + # 从 token 获取字段 csvfield = linetokens[csvidx] if csvfield == '': - # if empty ... assign the "nullvalue" + # 如果为空,赋值为 nullvalue csvfield = self.p.nullvalue - # get the corresponding line reference and set the value + # 获取对应 line 引用并设置 value line = getattr(self.lines, linefield) line[0] = float(float(csvfield)) diff --git a/backtrader/feeds/ibdata.py b/backtrader/feeds/ibdata.py index 66b012552..80d08bfe2 100644 --- a/backtrader/feeds/ibdata.py +++ b/backtrader/feeds/ibdata.py @@ -34,227 +34,144 @@ class MetaIBData(DataBase.__class__): def __init__(cls, name, bases, dct): - '''Class has already been created ... register''' - # Initialize the class + '''类已经创建完成,随后把它注册到对应 store。''' + # 初始化类对象 super(MetaIBData, cls).__init__(name, bases, dct) - # Register with the store + # 注册到 store,供 IBStore 找到实际 DataCls ibstore.IBStore.DataCls = cls class IBData(with_metaclass(MetaIBData, DataBase)): - '''Interactive Brokers Data Feed. + '''Interactive Brokers 数据源。 - Supports the following contract specifications in parameter ``dataname``: + Args: + dataname: IB contract 描述字符串。支持下列格式: - - TICKER # Stock type and SMART exchange - - TICKER-STK # Stock and SMART exchange + - TICKER # Stock 类型和 SMART 交易所 + - TICKER-STK # Stock 和 SMART 交易所 - TICKER-STK-EXCHANGE # Stock - TICKER-STK-EXCHANGE-CURRENCY # Stock - - TICKER-CFD # CFD and SMART exchange + - TICKER-CFD # CFD 和 SMART 交易所 - TICKER-CFD-EXCHANGE # CFD - TICKER-CDF-EXCHANGE-CURRENCY # Stock - - TICKER-IND-EXCHANGE # Index - - TICKER-IND-EXCHANGE-CURRENCY # Index + - TICKER-IND-EXCHANGE # 指数 + - TICKER-IND-EXCHANGE-CURRENCY # 指数 - - TICKER-YYYYMM-EXCHANGE # Future - - TICKER-YYYYMM-EXCHANGE-CURRENCY # Future - - TICKER-YYYYMM-EXCHANGE-CURRENCY-MULT # Future - - TICKER-FUT-EXCHANGE-CURRENCY-YYYYMM-MULT # Future + - TICKER-YYYYMM-EXCHANGE # 期货 + - TICKER-YYYYMM-EXCHANGE-CURRENCY # 期货 + - TICKER-YYYYMM-EXCHANGE-CURRENCY-MULT # 期货 + - TICKER-FUT-EXCHANGE-CURRENCY-YYYYMM-MULT # 期货 - TICKER-YYYYMM-EXCHANGE-CURRENCY-STRIKE-RIGHT # FOP - TICKER-YYYYMM-EXCHANGE-CURRENCY-STRIKE-RIGHT-MULT # FOP - TICKER-FOP-EXCHANGE-CURRENCY-YYYYMM-STRIKE-RIGHT # FOP - TICKER-FOP-EXCHANGE-CURRENCY-YYYYMM-STRIKE-RIGHT-MULT # FOP - - CUR1.CUR2-CASH-IDEALPRO # Forex + - CUR1.CUR2-CASH-IDEALPRO # 外汇 - TICKER-YYYYMMDD-EXCHANGE-CURRENCY-STRIKE-RIGHT # OPT - TICKER-YYYYMMDD-EXCHANGE-CURRENCY-STRIKE-RIGHT-MULT # OPT - TICKER-OPT-EXCHANGE-CURRENCY-YYYYMMDD-STRIKE-RIGHT # OPT - TICKER-OPT-EXCHANGE-CURRENCY-YYYYMMDD-STRIKE-RIGHT-MULT # OPT - Params: - - - ``sectype`` (default: ``STK``) - - Default value to apply as *security type* if not provided in the - ``dataname`` specification - - - ``exchange`` (default: ``SMART``) - - Default value to apply as *exchange* if not provided in the - ``dataname`` specification - - - ``currency`` (default: ``''``) - - Default value to apply as *currency* if not provided in the - ``dataname`` specification - - - ``historical`` (default: ``False``) - - If set to ``True`` the data feed will stop after doing the first - download of data. - - The standard data feed parameters ``fromdate`` and ``todate`` will be - used as reference. - - The data feed will make multiple requests if the requested duration is - larger than the one allowed by IB given the timeframe/compression - chosen for the data. - - - ``what`` (default: ``None``) - - If ``None`` the default for different assets types will be used for - historical data requests: - - - 'BID' for CASH assets - - 'TRADES' for any other - - Use 'ASK' for the Ask quote of cash assets - - Check the IB API docs if another value is wished - - - ``rtbar`` (default: ``False``) - - If ``True`` the ``5 Seconds Realtime bars`` provided by Interactive - Brokers will be used as the smalles tick. According to the - documentation they correspond to real-time values (once collated and - curated by IB) - - If ``False`` then the ``RTVolume`` prices will be used, which are based - on receiving ticks. In the case of ``CASH`` assets (like for example - EUR.JPY) ``RTVolume`` will always be used and from it the ``bid`` price - (industry de-facto standard with IB according to the literature - scattered over the Internet) - - Even if set to ``True``, if the data is resampled/kept to a - timeframe/compression below Seconds/5, no real time bars will be used, - because IB doesn't serve them below that level - - - ``qcheck`` (default: ``0.5``) - - Time in seconds to wake up if no data is received to give a chance to - resample/replay packets properly and pass notifications up the chain - - - ``backfill_start`` (default: ``True``) - - Perform backfilling at the start. The maximum possible historical data - will be fetched in a single request. - - - ``backfill`` (default: ``True``) - - Perform backfilling after a disconnection/reconnection cycle. The gap - duration will be used to download the smallest possible amount of data - - - ``backfill_from`` (default: ``None``) - - An additional data source can be passed to do an initial layer of - backfilling. Once the data source is depleted and if requested, - backfilling from IB will take place. This is ideally meant to backfill - from already stored sources like a file on disk, but not limited to. - - - ``latethrough`` (default: ``False``) - - If the data source is resampled/replayed, some ticks may come in too - late for the already delivered resampled/replayed bar. If this is - ``True`` those ticks will bet let through in any case. - - Check the Resampler documentation to see who to take those ticks into - account. - - This can happen especially if ``timeoffset`` is set to ``False`` in - the ``IBStore`` instance and the TWS server time is not in sync with - that of the local computer - - - ``tradename`` (default: ``None``) - Useful for some specific cases like ``CFD`` in which prices are offered - by one asset and trading happens in a different onel - - - SPY-STK-SMART-USD -> SP500 ETF (will be specified as ``dataname``) - - - SPY-CFD-SMART-USD -> which is the corresponding CFD which offers not - price tracking but in this case will be the trading asset (specified - as ``tradename``) - - The default values in the params are the to allow things like ```TICKER``, - to which the parameter ``sectype`` (default: ``STK``) and ``exchange`` - (default: ``SMART``) are applied. - - Some assets like ``AAPL`` need full specification including ``currency`` - (default: '') whereas others like ``TWTR`` can be simply passed as it is. - - - ``AAPL-STK-SMART-USD`` would be the full specification for dataname - - Or else: ``IBData`` as ``IBData(dataname='AAPL', currency='USD')`` - which uses the default values (``STK`` and ``SMART``) and overrides - the currency to be ``USD`` + sectype: ``dataname`` 未提供 security type 时使用的默认值,默认 ``STK``。 + exchange: ``dataname`` 未提供 exchange 时使用的默认值,默认 ``SMART``。 + currency: ``dataname`` 未提供 currency 时使用的默认值,默认空字符串。 + historical: 为 ``True`` 时,完成首次历史数据下载后停止。会使用标准 data feed + 参数 ``fromdate`` 和 ``todate`` 作为时间范围。若请求范围超过 IB 在当前 + timeframe/compression 下允许的范围,会拆成多次请求。 + what: 历史数据请求类型。``None`` 时按资产类型使用默认值:``CASH`` 使用 + ``'BID'``,其他资产使用 ``'TRADES'``。现金资产也可使用 ``'ASK'``。 + rtbar: 为 ``True`` 时使用 IB 的 ``5 Seconds Realtime bars``;为 ``False`` + 时使用基于 tick 的 ``RTVolume``。``CASH`` 资产始终使用 ``RTVolume``。 + useRTH: 历史数据是否仅下载 Regular Trading Hours。 + qcheck: 没有收到数据时的唤醒间隔(秒),用于给 resample/replay 和 notification + 传播留出处理机会。 + backfill_start: 启动时是否执行 backfill。会在单次请求中尽可能获取最大历史数据。 + backfill: 断线/重连后是否执行 backfill。会按缺口时长下载尽量小的数据范围。 + backfill_from: 额外的初始 backfill 数据源。该数据源耗尽后,如有需要,再从 IB + 拉取 backfill 数据。 + latethrough: resample/replay 后,如果 tick 晚于已输出的 bar,是否仍允许其通过。 + tradename: 与 ``dataname`` 不同的交易标的名称,常用于 CFD 等“报价资产”和 + “交易资产”不同的场景。 + + Returns: + IBData: 可加入 Cerebro 的 Interactive Brokers 数据源实例。 + + 默认参数允许像 ``TICKER`` 这样的简写形式,此时会自动套用 ``sectype='STK'`` 和 + ``exchange='SMART'``。例如 ``AAPL-STK-SMART-USD`` 是完整写法,也可以写成 + ``IBData(dataname='AAPL', currency='USD')``。 + + --- + 交互界面使用示范: + + >>> data = IBData(dataname='AAPL', currency='USD') # doctest: +SKIP + >>> data.p.currency # doctest: +SKIP + 'USD' ''' params = ( - ('sectype', 'STK'), # usual industry value - ('exchange', 'SMART'), # usual industry value + ('sectype', 'STK'), # 行业常用默认值 + ('exchange', 'SMART'), # 行业常用默认值 ('currency', ''), - ('rtbar', False), # use RealTime 5 seconds bars - ('historical', False), # only historical download - ('what', None), # historical - what to show - ('useRTH', False), # historical - download only Regular Trading Hours - ('qcheck', 0.5), # timeout in seconds (float) to check for events - ('backfill_start', True), # do backfilling at the start - ('backfill', True), # do backfilling when reconnecting - ('backfill_from', None), # additional data source to do backfill from - ('latethrough', False), # let late samples through - ('tradename', None), # use a different asset as order target + ('rtbar', False), # 使用 RealTime 5 秒 bar + ('historical', False), # 仅下载历史数据 + ('what', None), # 历史数据请求类型 + ('useRTH', False), # 历史数据仅下载 Regular Trading Hours + ('qcheck', 0.5), # 检查事件的超时时间(秒,float) + ('backfill_start', True), # 启动时执行 backfill + ('backfill', True), # 重连时执行 backfill + ('backfill_from', None), # 用于 backfill 的额外数据源 + ('latethrough', False), # 允许延迟样本通过 + ('tradename', None), # 使用不同资产作为下单目标 ) _store = ibstore.IBStore - # Minimum size supported by real-time bars + # 实时 bar 支持的最小周期 RTBAR_MINSIZE = (TimeFrame.Seconds, 5) - # States for the Finite State Machine in _load + # _load 中有限状态机的状态 _ST_FROM, _ST_START, _ST_LIVE, _ST_HISTORBACK, _ST_OVER = range(5) def _timeoffset(self): return self.ib.timeoffset() def _gettz(self): - # If no object has been provided by the user and a timezone can be - # found via contractdtails, then try to get it from pytz, which may or - # may not be available. + # 如果用户没有提供 timezone 对象,但 contractdetails 中可以找到 timezone, + # 就尝试通过 pytz 获取;pytz 可能不存在。 - # The timezone specifications returned by TWS seem to be abbreviations - # understood by pytz, but the full list which TWS may return is not - # documented and one of the abbreviations may fail + # TWS 返回的 timezone 看起来是 pytz 能理解的缩写,但 TWS 可能返回的完整列表 + # 没有文档保证,某些缩写可能失败 tzstr = isinstance(self.p.tz, string_types) if self.p.tz is not None and not tzstr: return bt.utils.date.Localizer(self.p.tz) if self.contractdetails is None: - return None # nothing can be done + return None # 无法继续处理 try: - import pytz # keep the import very local + import pytz # 保持局部导入 except ImportError: - return None # nothing can be done + return None # 无法继续处理 tzs = self.p.tz if tzstr else self.contractdetails.m_timeZoneId - if tzs == 'CST': # reported by TWS, not compatible with pytz. patch it + if tzs == 'CST': # TWS 返回值,与 pytz 不兼容,需要修正 tzs = 'CST6CDT' try: tz = pytz.timezone(tzs) except pytz.UnknownTimeZoneError: - return None # nothing can be done + return None # 无法继续处理 - # contractdetails there, import ok, timezone found, return it + # contractdetails 存在,导入成功,timezone 已找到,直接返回 return tz def islive(self): - '''Returns ``True`` to notify ``Cerebro`` that preloading and runonce - should be deactivated''' + '''返回 ``True``,通知 ``Cerebro`` 关闭 preload 和 runonce。''' return not self.p.historical def __init__(self, **kwargs): @@ -263,14 +180,13 @@ def __init__(self, **kwargs): self.pretradecontract = self.parsecontract(self.p.tradename) def setenvironment(self, env): - '''Receives an environment (cerebro) and passes it over to the store it - belongs to''' + '''接收 Cerebro 环境,并把它传给所属 store。''' super(IBData, self).setenvironment(env) env.addstore(self.ib) def parsecontract(self, dataname): - '''Parses dataname generates a default contract''' - # Set defaults for optional tokens in the ticker string + '''解析 dataname,并生成默认 contract。''' + # 为 ticker 字符串中的可选 token 设置默认值 if dataname is None: return None @@ -281,58 +197,58 @@ def parsecontract(self, dataname): right = '' mult = '' - # split the ticker string + # 拆分 ticker 字符串 tokens = iter(dataname.split('-')) - # Symbol and security type are compulsory + # symbol 和 security type 是必需字段 symbol = next(tokens) try: sectype = next(tokens) except StopIteration: sectype = self.p.sectype - # security type can be an expiration date + # security type 位置也可能是到期日 if sectype.isdigit(): - expiry = sectype # save the expiration ate + expiry = sectype # 保存到期日 if len(sectype) == 6: # YYYYMM sectype = 'FUT' - else: # Assume OPTIONS - YYYYMMDD + else: # 视为 OPTIONS - YYYYMMDD sectype = 'OPT' - if sectype == 'CASH': # need to address currency for Forex + if sectype == 'CASH': # Forex 需要拆出 currency symbol, curr = symbol.split('.') - # See if the optional tokens were provided + # 检查是否提供了可选 token try: - exch = next(tokens) # on exception it will be the default - curr = next(tokens) # on exception it will be the default + exch = next(tokens) # 异常时保持默认值 + curr = next(tokens) # 异常时保持默认值 if sectype == 'FUT': if not expiry: expiry = next(tokens) mult = next(tokens) - # Try to see if this is FOP - Futures on OPTIONS + # 尝试判断是否为 FOP - Futures on OPTIONS right = next(tokens) - # if still here this is a FOP and not a FUT + # 能走到这里说明是 FOP,而不是 FUT sectype = 'FOP' - strike, mult = float(mult), '' # assign to strike and void + strike, mult = float(mult), '' # 转给 strike,并清空 mult - mult = next(tokens) # try again to see if there is any + mult = next(tokens) # 再尝试读取 mult elif sectype == 'OPT': if not expiry: expiry = next(tokens) - strike = float(next(tokens)) # on exception - default - right = next(tokens) # on exception it will be the default + strike = float(next(tokens)) # 异常时保持默认值 + right = next(tokens) # 异常时保持默认值 - mult = next(tokens) # ?? no harm in any case + mult = next(tokens) # 即使不存在也无妨 except StopIteration: pass - # Make the initial contract + # 创建初始 contract precon = self.ib.makecontract( symbol=symbol, sectype=sectype, exch=exch, curr=curr, expiry=expiry, strike=strike, right=right, mult=mult) @@ -340,17 +256,16 @@ def parsecontract(self, dataname): return precon def start(self): - '''Starts the IB connecction and gets the real contract and - contractdetails if it exists''' + '''启动 IB 连接,并在存在时获取真实 contract 和 contractdetails。''' super(IBData, self).start() - # Kickstart store and get queue to wait on + # 启动 store,并获取后续等待的数据队列 self.qlive = self.ib.start(data=self) self.qhist = None self._usertvol = not self.p.rtbar tfcomp = (self._timeframe, self._compression) if tfcomp < self.RTBAR_MINSIZE: - # Requested timeframe/compression not supported by rtbars + # 请求的 timeframe/compression 不受 rtbar 支持 self._usertvol = True self.contract = None @@ -363,54 +278,53 @@ def start(self): self.p.backfill_from.setenvironment(self._env) self.p.backfill_from._start() else: - self._state = self._ST_START # initial state for _load - self._statelivereconn = False # if reconnecting in live state - self._subcription_valid = False # subscription state - self._storedmsg = dict() # keep pending live message (under None) + self._state = self._ST_START # _load 的初始状态 + self._statelivereconn = False # 是否在 live 状态下重连 + self._subcription_valid = False # 订阅状态 + self._storedmsg = dict() # 保存待处理的 live 消息(键为 None) if not self.ib.connected(): return self.put_notification(self.CONNECTED) - # get real contract details with real conId (contractId) + # 通过真实 conId(contractId)获取真实 contract details cds = self.ib.getContractDetails(self.precontract, maxcount=1) if cds is not None: cdetails = cds[0] self.contract = cdetails.contractDetails.m_summary self.contractdetails = cdetails.contractDetails else: - # no contract can be found (or many) + # 找不到 contract,或匹配到多个 self.put_notification(self.DISCONNECTED) return if self.pretradecontract is None: - # no different trading asset - default to standard asset + # 没有不同的交易资产,默认使用标准资产 self.tradecontract = self.contract self.tradecontractdetails = self.contractdetails else: - # different target asset (typical of some CDS products) - # use other set of details + # 交易目标资产不同(某些 CDS 产品常见),使用另一套 details cds = self.ib.getContractDetails(self.pretradecontract, maxcount=1) if cds is not None: cdetails = cds[0] self.tradecontract = cdetails.contractDetails.m_summary self.tradecontractdetails = cdetails.contractDetails else: - # no contract can be found (or many) + # 找不到 contract,或匹配到多个 self.put_notification(self.DISCONNECTED) return if self._state == self._ST_START: - self._start_finish() # to finish initialization + self._start_finish() # 完成初始化 self._st_start() def stop(self): - '''Stops and tells the store to stop''' + '''停止数据源,并通知 store 停止。''' super(IBData, self).stop() self.ib.stop() def reqdata(self): - '''request real-time data. checks cash vs non-cash) and param useRT''' + '''请求实时数据,并根据资产类型和 rtbar 参数选择订阅方式。''' if self.contract is None or self._subcription_valid: return @@ -423,7 +337,7 @@ def reqdata(self): return self.qlive def canceldata(self): - '''Cancels Market Data subscription, checking asset type and rtbar''' + '''取消 Market Data 订阅,并根据资产类型和 rtbar 参数选择取消方式。''' if self.contract is None: return @@ -437,7 +351,7 @@ def haslivedata(self): def _load(self): if self.contract is None or self._state == self._ST_OVER: - return False # nothing can be done + return False # 无法继续处理 while True: if self._state == self._ST_LIVE: @@ -448,11 +362,11 @@ def _load(self): if True: return None - # Code invalidated until further checking is done + # 这段代码在进一步检查前保持无效 if not self._statelivereconn: - return None # indicate timeout situation + return None # 表示超时 - # Awaiting data and nothing came in - fake it up until now + # 等待数据但没有收到,临时补到当前时间 dtend = self.num2date(date2num(datetime.datetime.utcnow())) dtbegin = None if len(self) > 1: @@ -472,15 +386,15 @@ def _load(self): self._state = self._ST_HISTORBACK self._statelivereconn = False - continue # to reenter the loop and hit st_historback + continue # 重新进入循环并命中 st_historback - if msg is None: # Conn broken during historical/backfilling + if msg is None: # historical/backfill 期间连接断开 self._subcription_valid = False self.put_notification(self.CONNBROKEN) - # Try to reconnect + # 尝试重连 if not self.ib.reconnect(resub=True): self.put_notification(self.DISCONNECTED) - return False # failed + return False # 失败 self._statelivereconn = self.p.backfill continue @@ -489,44 +403,44 @@ def _load(self): self.put_notification(self.NOTSUBSCRIBED) return False - elif msg == -1100: # conn broken - # Tell to wait for a message to do a backfill + elif msg == -1100: # 连接断开 + # 等待消息后再执行 backfill # self._state = self._ST_DISCONN self._subcription_valid = False self._statelivereconn = self.p.backfill continue - elif msg == -1102: # conn broken/restored tickerId maintained - # The message may be duplicated + elif msg == -1102: # 连接恢复,tickerId 保持 + # 消息可能重复 if not self._statelivereconn: self._statelivereconn = self.p.backfill continue - elif msg == -1101: # conn broken/restored tickerId gone - # The message may be duplicated + elif msg == -1101: # 连接恢复,tickerId 丢失 + # 消息可能重复 self._subcription_valid = False if not self._statelivereconn: self._statelivereconn = self.p.backfill - self.reqdata() # resubscribe + self.reqdata() # 重新订阅 continue - elif msg == -10225: # Bust event occurred, current subscription is deactivated. + elif msg == -10225: # 发生 Bust event,当前订阅失效 self._subcription_valid = False if not self._statelivereconn: self._statelivereconn = self.p.backfill - self.reqdata() # resubscribe + self.reqdata() # 重新订阅 continue elif isinstance(msg, integer_types): - # Unexpected notification for historical data skip it - # May be a "not connected not yet processed" + # 历史数据阶段收到意外 notification,跳过它 + # 可能是“尚未处理的未连接状态” self.put_notification(self.UNKNOWN, msg) continue - # Process the message according to expected return type + # 按预期返回类型处理消息 if not self._statelivereconn: if self._laststatus != self.LIVE: - if self.qlive.qsize() <= 1: # very short live queue + if self.qlive.qsize() <= 1: # live 队列很短 self.put_notification(self.LIVE) if self._usertvol: @@ -536,25 +450,25 @@ def _load(self): if ret: return True - # could not load bar ... go and get new one + # 当前消息无法形成 bar,继续取下一条 continue - # Fall through to processing reconnect - try to backfill - self._storedmsg[None] = msg # keep the msg + # 进入重连处理流程,尝试 backfill + self._storedmsg[None] = msg # 保存当前消息 - # else do a backfill + # 否则执行 backfill if self._laststatus != self.DELAYED: self.put_notification(self.DELAYED) dtend = None if len(self) > 1: - # len == 1 ... forwarded for the 1st time - # get begin date in utc-like format like msg.datetime + # len == 1 表示第一次转发 + # 获取类似 msg.datetime 的 UTC 风格起始时间 dtbegin = num2date(self.datetime[-1]) elif self.fromdate > float('-inf'): dtbegin = num2date(self.fromdate) else: # 1st bar and no begin set - # passing None to fetch max possible in 1 request + # 传 None 表示单次请求尽可能获取最大范围 dtbegin = None dtend = msg.datetime if self._usertvol else msg.time @@ -566,56 +480,56 @@ def _load(self): sessionend=self.p.sessionend) self._state = self._ST_HISTORBACK - self._statelivereconn = False # no longer in live + self._statelivereconn = False # 不再处于 live 重连状态 continue elif self._state == self._ST_HISTORBACK: msg = self.qhist.get() - if msg is None: # Conn broken during historical/backfilling - # Situation not managed. Simply bail out + if msg is None: # historical/backfill 期间连接断开 + # 未处理该情况,直接退出 self._subcription_valid = False self.put_notification(self.DISCONNECTED) - return False # error management cancelled the queue + return False # 错误处理取消了队列 - elif msg == -354: # Data not subscribed + elif msg == -354: # 未订阅数据 self._subcription_valid = False self.put_notification(self.NOTSUBSCRIBED) return False - elif msg == -420: # No permissions for the data + elif msg == -420: # 没有数据权限 self._subcription_valid = False self.put_notification(self.NOTSUBSCRIBED) return False elif isinstance(msg, integer_types): - # Unexpected notification for historical data skip it - # May be a "not connected not yet processed" + # 历史数据阶段收到意外 notification,跳过它 + # 可能是“尚未处理的未连接状态” self.put_notification(self.UNKNOWN, msg) continue if msg.date is not None: if self._load_rtbar(msg, hist=True): - return True # loading worked + return True # 加载成功 - # the date is from overlapping historical request + # 日期来自重叠的历史数据请求 continue - # End of histdata - if self.p.historical: # only historical + # 历史数据结束 + if self.p.historical: # 仅历史模式 self.put_notification(self.DISCONNECTED) - return False # end of historical + return False # 历史数据结束 - # Live is also wished - go for it + # 还需要进入 live 模式 self._state = self._ST_LIVE continue elif self._state == self._ST_FROM: if not self.p.backfill_from.next(): - # additional data source is consumed + # 额外 backfill 数据源已经耗尽 self._state = self._ST_START continue - # copy lines of the same name + # 复制同名 line for alias in self.lines.getlinealiases(): lsrc = getattr(self.p.backfill_from.lines, alias) ldst = getattr(self.lines, alias) @@ -646,32 +560,30 @@ def _st_start(self): sessionend=self.p.sessionend) self._state = self._ST_HISTORBACK - return True # continue before + return True # 前面继续 - # Live is requested + # 请求 live 数据 if not self.ib.reconnect(resub=True): self.put_notification(self.DISCONNECTED) self._state = self._ST_OVER - return False # failed - was so + return False # 失败 self._statelivereconn = self.p.backfill_start if self.p.backfill_start: self.put_notification(self.DELAYED) self._state = self._ST_LIVE - return True # no return before - implicit continue + return True # 前面没有返回时,隐式继续 def _load_rtbar(self, rtbar, hist=False): - # A complete 5 second bar made of real-time ticks is delivered and - # contains open/high/low/close/volume prices - # The historical data has the same data but with 'date' instead of - # 'time' for datetime + # 完整的 5 秒 bar 由实时 tick 聚合而来,包含 open/high/low/close/volume + # 历史数据包含相同字段,但 datetime 使用 'date' 而不是 'time' dt = date2num(rtbar.time if not hist else rtbar.date) if dt < self.lines.datetime[-1] and not self.p.latethrough: - return False # cannot deliver earlier than already delivered + return False # 不能输出早于已输出时间的 bar self.lines.datetime[0] = dt - # Put the tick into the bar + # 把 tick 写入 bar self.lines.open[0] = rtbar.open self.lines.high[0] = rtbar.high self.lines.low[0] = rtbar.low @@ -682,17 +594,15 @@ def _load_rtbar(self, rtbar, hist=False): return True def _load_rtvolume(self, rtvol): - # A single tick is delivered and is therefore used for the entire set - # of prices. Ideally the - # contains open/high/low/close/volume prices - # Datetime transformation + # 单个 tick 会用于整组价格字段;理想情况下它包含 open/high/low/close/volume + # 转换 datetime dt = date2num(rtvol.datetime) if dt < self.lines.datetime[-1] and not self.p.latethrough: - return False # cannot deliver earlier than already delivered + return False # 不能输出早于已输出时间的 bar self.lines.datetime[0] = dt - # Put the tick into the bar + # 把 tick 写入 bar tick = rtvol.price self.lines.open[0] = tick self.lines.high[0] = tick diff --git a/backtrader/feeds/influxfeed.py b/backtrader/feeds/influxfeed.py index 7f047b8ae..f6b538025 100644 --- a/backtrader/feeds/influxfeed.py +++ b/backtrader/feeds/influxfeed.py @@ -39,6 +39,35 @@ class InfluxDB(feed.DataBase): + '''从 InfluxDB 聚合读取 OHLCV 数据的 data feed。 + + Args: + dataname: InfluxDB 中的 measurement 名称。 + host: InfluxDB 主机地址,默认 ``127.0.0.1``。 + port: InfluxDB 端口,默认 ``8086``。 + username: 连接用户名。 + password: 连接密码。 + database: 要连接的数据库名。 + timeframe: 聚合输出的 timeframe。 + startdate: 查询起始时间;为空时查询到当前时间为止。 + high: high 字段名。 + low: low 字段名。 + open: open 字段名。 + close: close 字段名。 + volume: volume 字段名。 + ointerest: openinterest 字段名。 + + Returns: + InfluxDB: 可加入 Cerebro 的 InfluxDB 数据源实例。 + + --- + 交互界面使用示范: + + >>> data = InfluxDB(dataname='prices', database='market') # doctest: +SKIP + >>> data.p.database # doctest: +SKIP + 'market' + ''' + frompackages = ( ('influxdb', [('InfluxDBClient', 'idbclient')]), ('influxdb.exceptions', 'InfluxDBClientError') @@ -77,8 +106,8 @@ def start(self): else: st = '>= \'%s\'' % self.p.startdate - # The query could already consider parameters like fromdate and todate - # to have the database skip them and not the internal code + # 查询本身可以纳入 fromdate/todate 等参数,让数据库先过滤数据, + # 避免内部代码再做一次过滤 qstr = ('SELECT mean("{open_f}") AS "open", mean("{high_f}") AS "high", ' 'mean("{low_f}") AS "low", mean("{close_f}") AS "close", ' 'mean("{vol_f}") AS "volume", mean("{oi_f}") AS "openinterest" ' diff --git a/backtrader/feeds/mt4csv.py b/backtrader/feeds/mt4csv.py index 74dc7dbca..e94319de7 100644 --- a/backtrader/feeds/mt4csv.py +++ b/backtrader/feeds/mt4csv.py @@ -27,15 +27,20 @@ class MT4CSVData(GenericCSVData): - ''' - Parses a `Metatrader4 `_ History - center CSV exported file. + '''解析 `Metatrader4 `_ + History Center 导出的 CSV 文件。 + + 该类基于 ``GenericCSVData``,并调整日期、时间和字段位置参数以匹配 MT4 + 导出格式。 - Specific parameters (or specific meaning): + Args: + dataname: 要解析的文件名或 file-like 对象。 - - ``dataname``: The filename to parse or a file-like object + Returns: + bool: 成功解析一行时返回 ``True``。 - - Uses GenericCSVData and simply modifies the params + --- + >>> data = MT4CSVData(dataname='mt4-history.csv') ''' params = ( diff --git a/backtrader/feeds/oanda.py b/backtrader/feeds/oanda.py index 2581772ea..7b9f9f834 100644 --- a/backtrader/feeds/oanda.py +++ b/backtrader/feeds/oanda.py @@ -33,85 +33,40 @@ class MetaOandaData(DataBase.__class__): def __init__(cls, name, bases, dct): - '''Class has already been created ... register''' - # Initialize the class + '''类已经创建完成,随后把它注册到对应 store。''' + # 初始化类对象 super(MetaOandaData, cls).__init__(name, bases, dct) - # Register with the store + # 注册到 store,供 OandaStore 找到实际 DataCls oandastore.OandaStore.DataCls = cls class OandaData(with_metaclass(MetaOandaData, DataBase)): - '''Oanda Data Feed. - - Params: - - - ``qcheck`` (default: ``0.5``) - - Time in seconds to wake up if no data is received to give a chance to - resample/replay packets properly and pass notifications up the chain - - - ``historical`` (default: ``False``) - - If set to ``True`` the data feed will stop after doing the first - download of data. - - The standard data feed parameters ``fromdate`` and ``todate`` will be - used as reference. - - The data feed will make multiple requests if the requested duration is - larger than the one allowed by IB given the timeframe/compression - chosen for the data. - - - ``backfill_start`` (default: ``True``) - - Perform backfilling at the start. The maximum possible historical data - will be fetched in a single request. - - - ``backfill`` (default: ``True``) - - Perform backfilling after a disconnection/reconnection cycle. The gap - duration will be used to download the smallest possible amount of data - - - ``backfill_from`` (default: ``None``) - - An additional data source can be passed to do an initial layer of - backfilling. Once the data source is depleted and if requested, - backfilling from IB will take place. This is ideally meant to backfill - from already stored sources like a file on disk, but not limited to. - - - ``bidask`` (default: ``True``) - - If ``True``, then the historical/backfilling requests will request - bid/ask prices from the server - - If ``False``, then *midpoint* will be requested - - - ``useask`` (default: ``False``) - - If ``True`` the *ask* part of the *bidask* prices will be used instead - of the default use of *bid* - - - ``includeFirst`` (default: ``True``) - - Influence the delivery of the 1st bar of a historical/backfilling - request by setting the parameter directly to the Oanda API calls - - - ``reconnect`` (default: ``True``) - - Reconnect when network connection is down - - - ``reconnections`` (default: ``-1``) - - Number of times to attempt reconnections: ``-1`` means forever - - - ``reconntimeout`` (default: ``5.0``) - - Time in seconds to wait in between reconnection attemps - - This data feed supports only this mapping of ``timeframe`` and - ``compression``, which comply with the definitions in the OANDA API - Developer's Guid:: + '''Oanda 数据源。 + + Args: + dataname: Oanda instrument 名称,例如 ``EUR_USD``。 + qcheck: 没有收到数据时的唤醒间隔(秒),用于给 resample/replay 和 notification + 传播留出处理机会。 + historical: 为 ``True`` 时,完成首次历史数据下载后停止。会使用标准 data feed + 参数 ``fromdate`` 和 ``todate`` 作为时间范围。 + backfill_start: 启动时是否执行 backfill。会在单次请求中尽可能获取最大历史数据。 + backfill: 断线/重连后是否执行 backfill。会按缺口时长下载尽量小的数据范围。 + backfill_from: 额外的初始 backfill 数据源。该数据源耗尽后,如有需要,再从 + Oanda 拉取 backfill 数据。 + bidask: 历史/backfill 请求是否向服务器请求 bid/ask 价格;为 ``False`` 时请求 + midpoint。 + useask: 使用 bid/ask 数据时,是否使用 ask 侧价格;默认使用 bid。 + includeFirst: 直接传给 Oanda API,控制历史/backfill 请求的第一个 bar 是否返回。 + reconnect: 网络断开时是否重连。 + reconnections: 最大重连次数,``-1`` 表示无限重连。 + reconntimeout: 两次重连尝试之间等待的秒数。 + + Returns: + OandaData: 可加入 Cerebro 的 Oanda 数据源实例。 + + 支持的 ``timeframe`` / ``compression`` 组合如下,需符合 OANDA API Developer's + Guide 的 granularity 定义:: (TimeFrame.Seconds, 5): 'S5', (TimeFrame.Seconds, 10): 'S10', @@ -135,36 +90,42 @@ class OandaData(with_metaclass(MetaOandaData, DataBase)): (TimeFrame.Weeks, 1): 'W', (TimeFrame.Months, 1): 'M', - Any other combination will be rejected + 其他组合会被拒绝。 + + --- + 交互界面使用示范: + + >>> data = OandaData(dataname='EUR_USD') # doctest: +SKIP + >>> data.p.dataname # doctest: +SKIP + 'EUR_USD' ''' params = ( ('qcheck', 0.5), - ('historical', False), # do backfilling at the start - ('backfill_start', True), # do backfilling at the start - ('backfill', True), # do backfilling when reconnecting - ('backfill_from', None), # additional data source to do backfill from + ('historical', False), # 仅下载历史数据 + ('backfill_start', True), # 启动时执行 backfill + ('backfill', True), # 重连时执行 backfill + ('backfill_from', None), # 用于 backfill 的额外数据源 ('bidask', True), ('useask', False), ('includeFirst', True), ('reconnect', True), - ('reconnections', -1), # forever + ('reconnections', -1), # 无限重连 ('reconntimeout', 5.0), ) _store = oandastore.OandaStore - # States for the Finite State Machine in _load + # _load 中有限状态机的状态 _ST_FROM, _ST_START, _ST_LIVE, _ST_HISTORBACK, _ST_OVER = range(5) _TOFFSET = timedelta() def _timeoffset(self): - # Effective way to overcome the non-notification? + # 用于弥补未发送 notification 的时间偏移 return self._TOFFSET def islive(self): - '''Returns ``True`` to notify ``Cerebro`` that preloading and runonce - should be deactivated''' + '''返回 ``True``,通知 ``Cerebro`` 关闭 preload 和 runonce。''' return True def __init__(self, **kwargs): @@ -172,26 +133,24 @@ def __init__(self, **kwargs): self._candleFormat = 'bidask' if self.p.bidask else 'midpoint' def setenvironment(self, env): - '''Receives an environment (cerebro) and passes it over to the store it - belongs to''' + '''接收 Cerebro 环境,并把它传给所属 store。''' super(OandaData, self).setenvironment(env) env.addstore(self.o) def start(self): - '''Starts the Oanda connecction and gets the real contract and - contractdetails if it exists''' + '''启动 Oanda 连接,并在存在时获取真实 instrument 信息。''' super(OandaData, self).start() - # Create attributes as soon as possible - self._statelivereconn = False # if reconnecting in live state - self._storedmsg = dict() # keep pending live message (under None) + # 尽早创建运行期属性 + self._statelivereconn = False # 是否在 live 状态下重连 + self._storedmsg = dict() # 保存待处理的 live 消息(键为 None) self.qlive = queue.Queue() self._state = self._ST_OVER - # Kickstart store and get queue to wait on + # 启动 store,并获取后续等待的数据队列 self.o.start(data=self) - # check if the granularity is supported + # 检查 granularity 是否支持 otf = self.o.get_granularity(self._timeframe, self._compression) if otf is None: self.put_notification(self.NOTSUPPORTED_TF) @@ -209,7 +168,7 @@ def start(self): self.p.backfill_from._start() else: self._start_finish() - self._state = self._ST_START # initial state for _load + self._state = self._ST_START # _load 的初始状态 self._st_start() self._reconns = 0 @@ -247,15 +206,15 @@ def _st_start(self, instart=True, tmout=None): if instart: self._reconns = self.p.reconnections - return True # no return before - implicit continue + return True # 前面没有返回时,隐式继续 def stop(self): - '''Stops and tells the store to stop''' + '''停止数据源,并通知 store 停止。''' super(OandaData, self).stop() self.o.stop() def haslivedata(self): - return bool(self._storedmsg or self.qlive) # do not return the objs + return bool(self._storedmsg or self.qlive) # 不直接返回对象本身 def _load(self): if self._state == self._ST_OVER: @@ -267,16 +226,16 @@ def _load(self): msg = (self._storedmsg.pop(None, None) or self.qlive.get(timeout=self._qcheck)) except queue.Empty: - return None # indicate timeout situation + return None # 表示超时 - if msg is None: # Conn broken during historical/backfilling + if msg is None: # historical/backfill 期间连接断开 self.put_notification(self.CONNBROKEN) - # Try to reconnect + # 尝试重连 if not self.p.reconnect or self._reconns == 0: - # Can no longer reconnect + # 已无法继续重连 self.put_notification(self.DISCONNECTED) self._state = self._ST_OVER - return False # failed + return False # 失败 self._reconns -= 1 self._st_start(instart=False, tmout=self.p.reconntimeout) @@ -288,49 +247,49 @@ def _load(self): if code not in [599, 598, 596]: self.put_notification(self.DISCONNECTED) self._state = self._ST_OVER - return False # failed + return False # 失败 if not self.p.reconnect or self._reconns == 0: - # Can no longer reconnect + # 已无法继续重连 self.put_notification(self.DISCONNECTED) self._state = self._ST_OVER - return False # failed + return False # 失败 - # Can reconnect + # 可以继续重连 self._reconns -= 1 self._st_start(instart=False, tmout=self.p.reconntimeout) continue self._reconns = self.p.reconnections - # Process the message according to expected return type + # 按预期返回类型处理消息 if not self._statelivereconn: if self._laststatus != self.LIVE: - if self.qlive.qsize() <= 1: # very short live queue + if self.qlive.qsize() <= 1: # live 队列很短 self.put_notification(self.LIVE) ret = self._load_tick(msg) if ret: return True - # could not load bar ... go and get new one + # 当前消息无法形成 bar,继续取下一条 continue - # Fall through to processing reconnect - try to backfill - self._storedmsg[None] = msg # keep the msg + # 进入重连处理流程,尝试 backfill + self._storedmsg[None] = msg # 保存当前消息 - # else do a backfill + # 否则执行 backfill if self._laststatus != self.DELAYED: self.put_notification(self.DELAYED) dtend = None if len(self) > 1: - # len == 1 ... forwarded for the 1st time + # len == 1 表示第一次转发 dtbegin = self.datetime.datetime(-1) elif self.fromdate > float('-inf'): dtbegin = num2date(self.fromdate) else: # 1st bar and no begin set - # passing None to fetch max possible in 1 request + # 传 None 表示单次请求尽可能获取最大范围 dtbegin = None dtend = datetime.utcfromtimestamp(int(msg['time']) / 10 ** 6) @@ -342,18 +301,18 @@ def _load(self): includeFirst=self.p.includeFirst) self._state = self._ST_HISTORBACK - self._statelivereconn = False # no longer in live + self._statelivereconn = False # 不再处于 live 重连状态 continue elif self._state == self._ST_HISTORBACK: msg = self.qhist.get() - if msg is None: # Conn broken during historical/backfilling - # Situation not managed. Simply bail out + if msg is None: # historical/backfill 期间连接断开 + # 未处理该情况,直接退出 self.put_notification(self.DISCONNECTED) self._state = self._ST_OVER - return False # error management cancelled the queue + return False # 错误处理取消了队列 - elif 'code' in msg: # Error + elif 'code' in msg: # 错误 self.put_notification(self.NOTSUBSCRIBED) self.put_notification(self.DISCONNECTED) self._state = self._ST_OVER @@ -361,27 +320,27 @@ def _load(self): if msg: if self._load_history(msg): - return True # loading worked + return True # 加载成功 - continue # not loaded ... date may have been seen + continue # 未加载,日期可能已经见过 else: - # End of histdata - if self.p.historical: # only historical + # 历史数据结束 + if self.p.historical: # 仅历史模式 self.put_notification(self.DISCONNECTED) self._state = self._ST_OVER - return False # end of historical + return False # 历史数据结束 - # Live is also wished - go for it + # 还需要进入 live 模式 self._state = self._ST_LIVE continue elif self._state == self._ST_FROM: if not self.p.backfill_from.next(): - # additional data source is consumed + # 额外 backfill 数据源已经耗尽 self._state = self._ST_START continue - # copy lines of the same name + # 复制同名 line for alias in self.lines.getlinealiases(): lsrc = getattr(self.p.backfill_from.lines, alias) ldst = getattr(self.lines, alias) @@ -399,14 +358,14 @@ def _load_tick(self, msg): dtobj = datetime.utcfromtimestamp(int(msg['time']) / 10 ** 6) dt = date2num(dtobj) if dt <= self.lines.datetime[-1]: - return False # time already seen + return False # 时间已经处理过 - # Common fields + # 通用字段 self.lines.datetime[0] = dt self.lines.volume[0] = 0.0 self.lines.openinterest[0] = 0.0 - # Put the prices into the bar + # 把价格写入 bar tick = float(msg['ask']) if self.p.useask else float(msg['bid']) self.lines.open[0] = tick self.lines.high[0] = tick @@ -421,14 +380,14 @@ def _load_history(self, msg): dtobj = datetime.utcfromtimestamp(int(msg['time']) / 10 ** 6) dt = date2num(dtobj) if dt <= self.lines.datetime[-1]: - return False # time already seen + return False # 时间已经处理过 - # Common fields + # 通用字段 self.lines.datetime[0] = dt self.lines.volume[0] = float(msg['volume']) self.lines.openinterest[0] = 0.0 - # Put the prices into the bar + # 把价格写入 bar if self.p.bidask: if not self.p.useask: self.lines.open[0] = float(msg['openBid']) diff --git a/backtrader/feeds/pandafeed.py b/backtrader/feeds/pandafeed.py index cc90953ae..c79817715 100644 --- a/backtrader/feeds/pandafeed.py +++ b/backtrader/feeds/pandafeed.py @@ -29,19 +29,33 @@ class PandasDirectData(feed.DataBase): ''' - Uses a Pandas DataFrame as the feed source, iterating directly over the - tuples returned by "itertuples". - - This means that all parameters related to lines must have numeric - values as indices into the tuples - - Note: - - - The ``dataname`` parameter is a Pandas DataFrame - - - A negative value in any of the parameters for the Data lines - indicates it's not present in the DataFrame - it is + 使用 Pandas DataFrame 作为数据源,直接遍历 ``itertuples`` 返回的 tuple。 + + 因为直接读取 tuple,所有 line 相关参数都必须使用数字索引。 + + Args: + dataname: Pandas DataFrame。 + datetime: datetime 字段在 tuple 中的索引。 + open: open 字段在 tuple 中的索引。 + high: high 字段在 tuple 中的索引。 + low: low 字段在 tuple 中的索引。 + close: close 字段在 tuple 中的索引。 + volume: volume 字段在 tuple 中的索引。 + openinterest: openinterest 字段在 tuple 中的索引。任一 data line 参数为负数时, + 表示 DataFrame 中不存在对应字段。 + + Returns: + PandasDirectData: 可加入 Cerebro 的 Pandas 直读数据源实例。 + + --- + 交互界面使用示范: + + >>> class Frame: + ... def itertuples(self): + ... return iter(()) + >>> data = PandasDirectData(dataname=Frame()) + >>> data.p.open + 1 ''' params = ( @@ -61,7 +75,7 @@ class PandasDirectData(feed.DataBase): def start(self): super(PandasDirectData, self).start() - # reset the iterator on each start + # 每次 start 时重置迭代器 self._rows = self.p.dataname.itertuples() def _load(self): @@ -70,84 +84,87 @@ def _load(self): except StopIteration: return False - # Set the standard datafields - except for datetime + # 设置标准 datafield,datetime 单独处理 for datafield in self.getlinealiases(): if datafield == 'datetime': continue - # get the column index + # 读取字段所在的列索引 colidx = getattr(self.params, datafield) if colidx < 0: - # column not present -- skip + # DataFrame 中没有该列,跳过 continue - # get the line to be set + # 获取要写入的 line line = getattr(self.lines, datafield) - # indexing for pandas: 1st is colum, then row + # Pandas tuple 索引:直接按列位置取值 line[0] = row[colidx] - # datetime + # 处理 datetime 字段 colidx = getattr(self.params, 'datetime') tstamp = row[colidx] - # convert to float via datetime and store it + # 通过 datetime 转成浮点日期并存储 dt = tstamp.to_pydatetime() dtnum = date2num(dt) - # get the line to be set + # 获取要写入的 line line = getattr(self.lines, 'datetime') line[0] = dtnum - # Done ... return + # 当前 bar 加载完成 return True class PandasData(feed.DataBase): ''' - Uses a Pandas DataFrame as the feed source, using indices into column - names (which can be "numeric") - - This means that all parameters related to lines must have numeric - values as indices into the tuples - - Params: - - - ``nocase`` (default *True*) case insensitive match of column names - - Note: - - - The ``dataname`` parameter is a Pandas DataFrame - - - Values possible for datetime - - - None: the index contains the datetime - - -1: no index, autodetect column - - >= 0 or string: specific colum identifier - - - For other lines parameters - - - None: column not present - - -1: autodetect - - >= 0 or string: specific colum identifier + 使用 Pandas DataFrame 作为数据源,通过列名或列索引映射到各个 line。 + + Args: + dataname: Pandas DataFrame。 + nocase: 是否对列名进行大小写不敏感匹配,默认 ``True``。 + datetime: datetime 字段来源。``None`` 表示 DataFrame index 保存 datetime; + ``-1`` 表示自动检测列名;非负整数或字符串表示明确的列索引/列名。 + open: open 字段来源。``None`` 表示不存在,``-1`` 表示自动检测,非负整数或 + 字符串表示明确的列索引/列名。 + high: high 字段来源,含义同 ``open``。 + low: low 字段来源,含义同 ``open``。 + close: close 字段来源,含义同 ``open``。 + volume: volume 字段来源,含义同 ``open``。 + openinterest: openinterest 字段来源,含义同 ``open``。 + + Returns: + PandasData: 可加入 Cerebro 的 Pandas 数据源实例。 + + --- + 交互界面使用示范: + + >>> class Columns: + ... values = ['datetime', 'open', 'high', 'low', 'close', 'volume'] + >>> class Frame: + ... columns = Columns() + >>> data = PandasData(dataname=Frame()) + >>> data._colmapping['open'] + 'open' ''' params = ( ('nocase', True), - # Possible values for datetime (must always be present) - # None : datetime is the "index" in the Pandas Dataframe - # -1 : autodetect position or case-wise equal name - # >= 0 : numeric index to the colum in the pandas dataframe - # string : column name (as index) in the pandas dataframe + # datetime 的可选值(必须能找到) + # None : datetime 存在于 Pandas DataFrame 的 index 中 + # -1 : 自动检测位置或大小写匹配的同名列 + # >= 0 : Pandas DataFrame 中的数字列索引 + # string : Pandas DataFrame 中的列名 ('datetime', None), - # Possible values below: - # None : column not present - # -1 : autodetect position or case-wise equal name - # >= 0 : numeric index to the colum in the pandas dataframe - # string : column name (as index) in the pandas dataframe + # 下列字段的可选值: + # None : 不存在对应列 + # -1 : 自动检测位置或大小写匹配的同名列 + # >= 0 : Pandas DataFrame 中的数字列索引 + # string : Pandas DataFrame 中的列名 ('open', -1), ('high', -1), ('low', -1), @@ -163,25 +180,25 @@ class PandasData(feed.DataBase): def __init__(self): super(PandasData, self).__init__() - # these "colnames" can be strings or numeric types + # colnames 可以是字符串,也可以是数字类型 colnames = list(self.p.dataname.columns.values) if self.p.datetime is None: - # datetime is expected as index col and hence not returned + # datetime 预期在 index 中,因此不会出现在 columns 中 pass - # try to autodetect if all columns are numeric + # 尝试判断所有列名是否都是数字 cstrings = filter(lambda x: isinstance(x, string_types), colnames) colsnumeric = not len(list(cstrings)) - # Where each datafield find its value + # 每个 datafield 对应到哪个列 self._colmapping = dict() - # Build the column mappings to internal fields in advance + # 提前构建外部列到内部 line 的映射 for datafield in self.getlinealiases(): defmapping = getattr(self.params, datafield) if isinstance(defmapping, integer_types) and defmapping < 0: - # autodetection requested + # 请求自动检测 for colname in colnames: if isinstance(colname, string_types): if self.p.nocase: @@ -194,20 +211,20 @@ def __init__(self): break if datafield not in self._colmapping: - # autodetection requested and not found + # 请求自动检测但没有找到对应列 self._colmapping[datafield] = None continue else: - # all other cases -- used given index + # 其他情况直接使用给定索引或列名 self._colmapping[datafield] = defmapping def start(self): super(PandasData, self).start() - # reset the length with each start + # 每次 start 时重置行位置 self._idx = -1 - # Transform names (valid for .ix) into indices (good for .iloc) + # 把列名转换为适合 .iloc 使用的列索引 if self.p.nocase: colnames = [x.lower() for x in self.p.dataname.columns.values] else: @@ -215,7 +232,7 @@ def start(self): for k, v in self._colmapping.items(): if v is None: - continue # special marker for datetime + continue # datetime 或缺失字段的特殊标记 if isinstance(v, string_types): try: if self.p.nocase: @@ -227,7 +244,7 @@ def start(self): if isinstance(defmap, integer_types) and defmap < 0: v = None else: - raise e # let user now something failed + raise e # 让用户看到具体失败原因 self._colmapping[k] = v @@ -235,39 +252,39 @@ def _load(self): self._idx += 1 if self._idx >= len(self.p.dataname): - # exhausted all rows + # 所有行已经读完 return False - # Set the standard datafields + # 设置标准 datafield for datafield in self.getlinealiases(): if datafield == 'datetime': continue colindex = self._colmapping[datafield] if colindex is None: - # datafield signaled as missing in the stream: skip it + # 数据流中没有该 datafield,跳过 continue - # get the line to be set + # 获取要写入的 line line = getattr(self.lines, datafield) - # indexing for pandas: 1st is colum, then row + # Pandas iloc 索引:先行后列 line[0] = self.p.dataname.iloc[self._idx, colindex] - # datetime conversion + # 转换 datetime coldtime = self._colmapping['datetime'] if coldtime is None: - # standard index in the datetime + # datetime 使用标准 index tstamp = self.p.dataname.index[self._idx] else: - # it's in a different column ... use standard column index + # datetime 在普通列中,使用对应列索引 tstamp = self.p.dataname.iloc[self._idx, coldtime] - # convert to float via datetime and store it + # 通过 datetime 转成浮点日期并存储 dt = tstamp.to_pydatetime() dtnum = date2num(dt) self.lines.datetime[0] = dtnum - # Done ... return + # 当前 bar 加载完成 return True diff --git a/backtrader/feeds/quandl.py b/backtrader/feeds/quandl.py index 08ea1b2c3..1f66ed73f 100644 --- a/backtrader/feeds/quandl.py +++ b/backtrader/feeds/quandl.py @@ -38,33 +38,28 @@ class QuandlCSV(feed.CSVDataBase): ''' - Parses pre-downloaded Quandl CSV Data Feeds (or locally generated if they - comply to the Quandl format) - - Specific parameters: - - - ``dataname``: The filename to parse or a file-like object - - - ``reverse`` (default: ``False``) - - It is assumed that locally stored files have already been reversed - during the download process - - - ``adjclose`` (default: ``True``) - - Whether to use the dividend/split adjusted close and adjust all - values according to it. - - - ``round`` (default: ``False``) - - Whether to round the values to a specific number of decimals after - having adjusted the close - - - ``decimals`` (default: ``2``) - - Number of decimals to round to + 解析已经下载好的 Quandl CSV data feed;本地生成但符合 Quandl 格式的 CSV + 也可以使用。 + + Args: + dataname: 要解析的文件名,或已经打开的类文件对象。 + reverse: 是否反转本地文件顺序,默认 ``False``。通常认为本地保存的文件在下载 + 阶段已经处理过顺序。 + adjclose: 是否使用经过分红/拆股调整的 close,并据此调整全部价格字段。 + round: 是否在调整 close 后按指定小数位四舍五入。 + decimals: ``round`` 启用时保留的小数位数。 + + Returns: + QuandlCSV: 可加入 Cerebro 的 Quandl CSV 数据源实例。 + + --- + 交互界面使用示范: + + >>> data = QuandlCSV(dataname='WIKI-AAPL.csv', reverse=True) + >>> data.p.reverse + True ''' - _online = False # flag to avoid double reversal + _online = False # 避免重复反转的标记 params = ( ('reverse', False), @@ -79,9 +74,9 @@ def start(self): if not self.params.reverse: return elif self._online: - return # revers is True but also online, managed with order=asc + return # reverse 为 True 且在线下载时,已通过 order=asc 处理 - # Quandl data can be in reverse order -> reverse + # Quandl 数据可能倒序排列,需要反转 dq = collections.deque() for line in self.f: dq.appendleft(line) @@ -95,14 +90,14 @@ def start(self): def _loadline(self, linetokens): i = itertools.count(0) - dttxt = linetokens[next(i)] # YYYY-MM-DD + dttxt = linetokens[next(i)] # YYYY-MM-DD 格式 dt = date(int(dttxt[0:4]), int(dttxt[5:7]), int(dttxt[8:10])) dtnum = date2num(datetime.combine(dt, self.p.sessionend)) self.lines.datetime[0] = dtnum if self.p.adjclose: for _ in range(7): - next(i) # skip ohlcv, ex-dividend, split ratio + next(i) # 跳过 ohlcv、除息、拆股比例 o = float(linetokens[next(i)]) h = float(linetokens[next(i)]) @@ -130,52 +125,32 @@ def _loadline(self, linetokens): class Quandl(QuandlCSV): ''' - Executes a direct download of data from Quandl servers for the given time - range. - - Specific parameters (or specific meaning): - - - ``dataname`` - - The ticker to download ('YHOO' for example) - - - ``baseurl`` - - The server url. Someone might decide to open a Quandl compatible - service in the future. - - - ``proxies`` - - A dict indicating which proxy to go through for the download as in - {'http': 'http://myproxy.com'} or {'http': 'http://127.0.0.1:8080'} - - - ``buffered`` - - If True the entire socket connection wil be buffered locally before - parsing starts. - - - ``reverse`` - - Quandl returns the value in descending order (newest first). If this is - ``True`` (the default), the request will tell Quandl to return in - ascending (oldest to newest) format - - - ``adjclose`` - - Whether to use the dividend/split adjusted close and adjust all values - according to it. - - - ``apikey`` - - apikey identification in case it may be needed - - - ``dataset`` - - string identifying the dataset to query. Defaults to ``WIKI`` - + 按指定时间范围直接从 Quandl 服务器下载数据。 + + Args: + dataname: 要下载的 ticker,例如 ``'YHOO'``。 + baseurl: 服务端 URL。未来也可以指向兼容 Quandl 格式的服务。 + proxies: 下载时使用的代理字典,例如 + ``{'http': 'http://127.0.0.1:8080'}``。 + buffered: 是否先把整个 socket 返回内容缓冲到本地,再开始解析。 + reverse: Quandl 默认返回倒序数据。为 ``True`` 时,请求会要求 Quandl 返回 + 升序数据,也就是从旧到新。 + adjclose: 是否使用经过分红/拆股调整的 close,并据此调整全部价格字段。 + apikey: 需要时传入 Quandl API key。 + dataset: 要查询的数据集名称,默认 ``WIKI``。 + + Returns: + Quandl: 可加入 Cerebro 的 Quandl 在线数据源实例。 + + --- + 交互界面使用示范: + + >>> data = Quandl(dataname='YHOO', dataset='WIKI') + >>> data.p.dataset + 'WIKI' ''' - _online = True # flag to avoid double reversal + _online = True # 避免重复反转的标记 params = ( ('baseurl', 'https://www.quandl.com/api/v3/datasets'), @@ -219,15 +194,15 @@ def start(self): datafile = urlopen(url) except IOError as e: self.error = str(e) - # leave us empty + # 保持空数据源 return if datafile.headers['Content-Type'] != 'text/csv': self.error = 'Wrong content type: %s' % datafile.headers - return # HTML returned? wrong url? + return # 返回了 HTML,通常表示 URL 不正确 if self.params.buffered: - # buffer everything from the socket into a local buffer + # 把 socket 返回内容全部缓冲到本地 f = io.StringIO(datafile.read().decode('utf-8'), newline=None) datafile.close() else: @@ -235,5 +210,5 @@ def start(self): self.f = f - # Prepared a "path" file - CSV Parser can take over + # 已准备好类文件对象,交给 CSV parser 接管 super(Quandl, self).start() diff --git a/backtrader/feeds/rollover.py b/backtrader/feeds/rollover.py index 689118d27..3dc0b5452 100644 --- a/backtrader/feeds/rollover.py +++ b/backtrader/feeds/rollover.py @@ -29,13 +29,13 @@ class MetaRollOver(bt.DataBase.__class__): def __init__(cls, name, bases, dct): - '''Class has already been created ... register''' - # Initialize the class + '''类已经创建完成,随后进行初始化注册。''' + # 初始化类对象 super(MetaRollOver, cls).__init__(name, bases, dct) def donew(cls, *args, **kwargs): - '''Intercept const. to copy timeframe/compression from 1st data''' - # Create the object and set the params in place + '''拦截构造过程,从第一个 data 复制 timeframe/compression。''' + # 创建对象并设置参数 _obj, args, kwargs = super(MetaRollOver, cls).donew(*args, **kwargs) if args: @@ -46,69 +46,36 @@ def donew(cls, *args, **kwargs): class RollOver(bt.with_metaclass(MetaRollOver, bt.DataBase)): - '''Class that rolls over to the next future when a condition is met - - Params: - - - ``checkdate`` (default: ``None``) - - This must be a *callable* with the following signature:: - - checkdate(dt, d): - - Where: - - - ``dt`` is a ``datetime.datetime`` object - - ``d`` is the current data feed for the active future - - Expected Return Values: - - - ``True``: as long as the callable returns this, a switchover can - happen to the next future - - If a commodity expires on the 3rd Friday of March, ``checkdate`` could - return ``True`` for the entire week in which the expiration takes - place. - - - ``False``: the expiration cannot take place - - - ``checkcondition`` (default: ``None``) - - **Note**: This will only be called if ``checkdate`` has returned - ``True`` - - If ``None`` this will evaluate to ``True`` (execute roll over) - internally - - Else this must be a *callable* with this signature:: - - checkcondition(d0, d1) - - Where: - - - ``d0`` is the current data feed for the active future - - ``d1`` is the data feed for the next expiration - - Expected Return Values: - - - ``True``: roll-over to the next future - - Following with the example from ``checkdate``, this could say that the - roll-over can only happend if the *volume* from ``d0`` is already less - than the volume from ``d1`` - - - ``False``: the expiration cannot take place + '''在满足条件时切换到下一份期货合约的数据源。 + + Args: + *args: 按合约顺序传入的多个 data feed,当前合约满足切换规则后会切到下一个。 + checkdate: 可调用对象,签名为 ``checkdate(dt, d)``。``dt`` 是当前 active + data 的 ``datetime.datetime``,``d`` 是当前 active data feed。只要返回 + ``True``,就允许进入切换判断窗口。 + checkcondition: 可调用对象,签名为 ``checkcondition(d0, d1)``。只有 + ``checkdate`` 返回 ``True`` 时才会调用。``d0`` 是当前 active data, + ``d1`` 是下一份到期合约 data。返回 ``True`` 时执行 roll-over。 + + Returns: + RollOver: 可加入 Cerebro 的连续期货 roll-over 数据源实例。 + + --- + 交互界面使用示范: + + >>> data = RollOver(checkdate=lambda dt, d: False) + >>> data.p.checkdate is not None + True ''' params = ( - # ('rolls', []), # array of futures to roll over + # ('rolls', []), # 待 roll-over 的期货数据数组 ('checkdate', None), # callable ('checkcondition', None), # callable ) def islive(self): - '''Returns ``True`` to notify ``Cerebro`` that preloading and runonce - should be deactivated''' + '''返回 ``True``,通知 ``Cerebro`` 关闭 preload 和 runonce。''' return True def __init__(self, *args): @@ -120,7 +87,7 @@ def start(self): d.setenvironment(self._env) d._start() - # put the references in a separate list to have pops + # 把引用放到单独列表中,便于按顺序 pop self._ds = list(self._rolls) self._d = self._ds.pop(0) if self._ds else None self._dexp = None @@ -132,8 +99,7 @@ def stop(self): d.stop() def _gettz(self): - '''To be overriden by subclasses which may auto-calculate the - timezone''' + '''供子类覆写,用于自动计算 timezone。''' if self._rolls: return self._rolls[0]._gettz() return bt.utils.date.Localizer(self.p.tz) @@ -153,9 +119,9 @@ def _checkcondition(self, d0, d1): def _load(self): while self._d is not None: _next = self._d.next() - if _next is None: # no values yet, more will come + if _next is None: # 暂时没有值,后续还会有 continue - if _next is False: # no values from current data src + if _next is False: # 当前数据源已经没有值 if self._ds: self._d = self._ds.pop(0) self._dts.pop(0) @@ -163,9 +129,9 @@ def _load(self): self._d = None continue - dt0 = self._d.datetime.datetime() # current dt for active data + dt0 = self._d.datetime.datetime() # 当前 active data 的时间 - # Synchronize other datas using dt0 + # 使用 dt0 同步其他 data for i, d_dt in enumerate(zip(self._ds, self._dts)): d, dt = d_dt while dt < dt0: @@ -173,7 +139,7 @@ def _load(self): continue self._dts[i] = dt = d.datetime.datetime() - # Move expired future as much as needed + # 推进已经过期的合约,直到追上当前时间或耗尽 while self._dexp is not None: if not self._dexp.next(): self._dexp = None @@ -183,15 +149,14 @@ def _load(self): continue if self._dexp is None and self._checkdate(dt0, self._d): - # rule has been met ... check other factors only if 2 datas - # still there + # 日期规则已满足;只有还有下一份 data 时才检查其他条件 if self._ds and self._checkcondition(self._d, self._ds[0]): - # Time to switch to next data + # 可以切换到下一份 data self._dexp = self._d self._d = self._ds.pop(0) self._dts.pop(0) - # Fill the line and tell we die + # 填充当前 line,并返回已加载 self.lines.datetime[0] = self._d.lines.datetime[0] self.lines.open[0] = self._d.lines.open[0] self.lines.high[0] = self._d.lines.high[0] @@ -201,5 +166,5 @@ def _load(self): self.lines.openinterest[0] = self._d.lines.openinterest[0] return True - # Out of the loop -> self._d is None, no data feed to return from + # 退出循环表示 self._d 为 None,没有 data feed 可继续返回 return False diff --git a/backtrader/feeds/sierrachart.py b/backtrader/feeds/sierrachart.py index ce12323ce..e956b5c87 100644 --- a/backtrader/feeds/sierrachart.py +++ b/backtrader/feeds/sierrachart.py @@ -26,14 +26,19 @@ class SierraChartCSVData(GenericCSVData): - ''' - Parses a `SierraChart `_ CSV exported file. + '''解析 `SierraChart `_ 导出的 CSV 文件。 + + 该类基于 ``GenericCSVData``,并将 ``dtformat`` 调整为 SierraChart 常见的 + ``'%Y/%m/%d'``。 - Specific parameters (or specific meaning): + Args: + dataname: 要解析的文件名或 file-like 对象。 - - ``dataname``: The filename to parse or a file-like object + Returns: + bool: 成功解析一行时返回 ``True``。 - - Uses GenericCSVData and simply modifies the dateformat (dtformat) to + --- + >>> data = SierraChartCSVData(dataname='sierra.csv') ''' params = (('dtformat', '%Y/%m/%d'),) diff --git a/backtrader/feeds/vcdata.py b/backtrader/feeds/vcdata.py index f60a0e760..6fdcd0cc0 100644 --- a/backtrader/feeds/vcdata.py +++ b/backtrader/feeds/vcdata.py @@ -36,90 +36,68 @@ class MetaVCData(DataBase.__class__): def __init__(cls, name, bases, dct): - '''Class has already been created ... register''' - # Initialize the class + '''类已经创建完成,随后把它注册到对应 store。''' + # 初始化类对象 super(MetaVCData, cls).__init__(name, bases, dct) - # Register with the store + # 注册到 store,供 VCStore 找到实际 DataCls vcstore.VCStore.DataCls = cls class VCData(with_metaclass(MetaVCData, DataBase)): - '''VisualChart Data Feed. - - Params: - - - ``qcheck`` (default: ``0.5``) - Default timeout for waking up to let a resampler/replayer that the - current bar can be check for due delivery - - The value is only used if a resampling/replaying filter has been - inserted in the data - - - ``historical`` (default: ``False``) - If no ``todate`` parameter is supplied (defined in the base class), - this will force a historical only download if set to ``True`` - - If ``todate`` is supplied the same effect is achieved - - - ``milliseconds`` (default: ``True``) - The bars constructed by *Visual Chart* have this aspect: - HH:MM:59.999000 - - If this parameter is ``True`` a millisecond will be added to this time - to make it look like: HH::MM + 1:00.000000 - - - ``tradename`` (default: ``None``) - Continous futures cannot be traded but are ideal for data tracking. If - this parameter is supplied it will be the name of the current future - which will be the trading asset. Example: - - - 001ES -> ES-Mini continuous supplied as ``dataname`` - - - ESU16 -> ES-Mini 2016-09. If this is supplied in ``tradename`` it - will be the trading asset. - - - ``usetimezones`` (default: ``True``) - For most markets the time offset information provided by *Visual Chart* - allows for datetime to be converted to market time (*backtrader* choice - for representation) - - Some markets are special (``096``) and need special internal coverage - and timezone support to display in the user expected market time. - - If this parameter is set to ``True`` importing ``pytz`` will be - attempted to use timezones (default) - - Disabling it will remove timezone usage (may help if the load is - excesive) + '''VisualChart 数据源。 + + Args: + dataname: VisualChart symbol 名称。 + qcheck: resample/replay 场景下的默认唤醒超时时间,用于检查当前 bar 是否已经 + 可以交付。只有插入 resampling/replaying filter 时才使用。 + historical: 若没有提供基类参数 ``todate``,设置为 ``True`` 会强制只下载历史数据; + 如果提供了 ``todate``,也会达到相同效果。 + millisecond: VisualChart 构造的 bar 时间可能形如 ``HH:MM:59.999000``。 + 为 ``True`` 时增加 1 毫秒,使其看起来像下一分钟的 ``00.000000``。 + tradename: 连续期货适合跟踪数据但不能交易;可用该参数指定当前可交易期货作为 + 交易资产。 + usetimezones: 是否尝试导入 ``pytz`` 并使用 timezone。多数市场可通过 + VisualChart 的 time offset 转为市场时间;部分特殊市场(如 ``096``)需要 + 内部补偿和 timezone 支持。 + + Returns: + VCData: 可加入 Cerebro 的 VisualChart 数据源实例。 + + --- + 交互界面使用示范: + + >>> data = VCData(dataname='001ES') # doctest: +SKIP + >>> data.p.dataname # doctest: +SKIP + '001ES' ''' params = ( - ('qcheck', 0.5), # timeout in seconds (float) to check for events - ('historical', False), # usual industry value - ('millisecond', True), # fix missing millisecond in time - ('tradename', None), # name of the real asset to trade on - ('usetimezones', True), # use pytz timezones if found + ('qcheck', 0.5), # 检查事件的超时时间(秒,float) + ('historical', False), # 行业常用默认值 + ('millisecond', True), # 修正时间中缺失的 millisecond + ('tradename', None), # 实际交易资产名称 + ('usetimezones', True), # 找到 pytz timezone 时使用它 ) - # Holds the calculated offset to the timestamps of the VC Server + # 保存本地时间到 VC Server 时间戳的偏移 _TOFFSET = timedelta() - # States for the Finite State Machine in _load + # _load 中有限状态机的状态 _ST_START, _ST_FEEDING, _ST_NOTFOUND = range(3) - # Base NULL Date for VB/Excel date compatibility + # 为兼容 VB/Excel 日期而使用的空日期基准 NULLDATE = datetime(1899, 12, 30, 0, 0, 0) - # To correct HH:MM:59.999 times + # 用于修正 HH:MM:59.999 时间 MILLISECOND = timedelta(microseconds=1000) - # Large ping timeout + # 较长的 ping 超时时间 PING_TIMEOUT = 25.0 - # Timezones for the different exchanges + # 不同交易所对应的 timezone _TZS = { 'Europe/London': ('011', '024', '027', '036', '049', '092', '114', - # These are the global markets + # 这些是 global markets '033', '034', '035', '043', '054', '096', '300',), 'Europe/Berlin': ('005', '006', '008', '012', '013', '014', '015', @@ -140,7 +118,7 @@ class VCData(with_metaclass(MetaVCData, DataBase)): 'US/Central': ('001', '002', '020', '021', '022', '023', '056',), } - # The global assets may have a different output timezoe + # global assets 可能有不同的输出 timezone _TZOUT = { '096.FTSE': 'Europe/London', '096.FTEU3': 'Europe/London', @@ -154,8 +132,7 @@ class VCData(with_metaclass(MetaVCData, DataBase)): '096.NDX': 'US/Eastern', } - # These global markets deliver data in local time dst adjuste unlike those - # from above and need a readjustment + # 这些 global markets 返回的是本地 DST 调整后的时间,与上面的不同,需要再修正 _EXTRA_TIMEOFFSET = ('096',) _TIMEFRAME_BACKFILL = { @@ -170,42 +147,39 @@ class VCData(with_metaclass(MetaVCData, DataBase)): } def _timeoffset(self): - '''Returns the calculated time offset local equipment -> data server''' + '''返回本地设备到数据服务器之间计算出的时间偏移。''' return self._TOFFSET def _gettzinput(self): - '''Returns the timezone to consider for the input data''' + '''返回输入数据应使用的 timezone。''' return self._gettz(tzin=True) def _gettz(self, tzin=False): - '''Returns the default output timezone for the data + '''返回数据默认输出 timezone。 - This defaults to be the timezone in which the market is traded + 默认返回市场实际交易所在地的 timezone。 ''' - # If no object has been provided by the user and a timezone can be - # found via contractdtails, then try to get it from pytz, which may or - # may not be available. + # 如果用户没有提供 timezone 对象,就按市场代码尝试通过 pytz 获取; + # pytz 可能不存在。 - # The timezone specifications returned by TWS seem to be abbreviations - # understood by pytz, but the full list which TWS may return is not - # documented and one of the abbreviations may fail + # 市场 timezone 表可能无法覆盖全部返回值,某些缩写可能无法被 pytz 识别 ptz = self.p.tz tzstr = isinstance(ptz, string_types) if ptz is not None and not tzstr: return bt.utils.date.Localizer(ptz) if self._state == self._ST_NOTFOUND: - return None # nothing else can be done + return None # 无法继续处理 if not self.p.usetimezones: return None try: - import pytz # keep the import very local + import pytz # 保持局部导入 except ImportError: - return None # nothing can be done + return None # 无法继续处理 - # dataname 010ABCXXXXX -> ABC (3, 4 and 5) is market code + # dataname 010ABCXXXXX -> ABC(第 3、4、5 位)是市场代码 if tzstr: tzs = ptz else: @@ -231,22 +205,21 @@ def _gettz(self, tzin=False): try: tz = pytz.timezone(tzs) except pytz.UnknownTimeZoneError: - return None # nothing can be done + return None # 无法继续处理 else: return None - # contractdetails there, import ok, timezone found, return it + # 已找到 timezone,直接返回 return tz def islive(self): - '''Returns ``True`` to notify ``Cerebro`` that preloading and runonce - should be deactivated''' + '''返回 ``True``,通知 ``Cerebro`` 关闭 preload 和 runonce。''' return True def __init__(self, **kwargs): self.store = vcstore.VCStore(**kwargs) - # Correct a copy past directly from VisualChart + # 修正从 VisualChart 直接复制出来的 symbol dataname = self.p.dataname if dataname[3].isspace(): dataname = dataname[0:2] + dataname[4:] @@ -256,92 +229,81 @@ def __init__(self, **kwargs): self._mktcode = self.p.dataname[0:3] self._tradename = tradename = self.p.tradename or self._dataname - # Correct a copy past directly from VisualChart + # 修正从 VisualChart 直接复制出来的 tradename if tradename[3].isspace(): tradename = tradename[0:2] + tradename[4:] self._tradename = tradename def setenvironment(self, env): - '''Receives an environment (cerebro) and passes it over to the store it - belongs to''' + '''接收 Cerebro 环境,并把它传给所属 store。''' super(VCData, self).setenvironment(env) env.addstore(self.store) def start(self): - '''Starts the VC connecction and gets the real contract and - contractdetails if it exists''' + '''启动 VC 连接,并在存在时获取真实 symbol 信息。''' super(VCData, self).start() - self._state = self._ST_START # mini finite state machine + self._state = self._ST_START # 小型有限状态机 - self._newticks = True # control processing of initial ticks + self._newticks = True # 控制初始 tick 的处理 - self._pingtmout = self.PING_TIMEOUT # Initial timeout for ping + self._pingtmout = self.PING_TIMEOUT # 初始 ping 超时 - self.idx = 1 # counter for the dataserie (vb is based at 1) - self.q = None # where bars are received + self.idx = 1 # dataserie 计数器(VB 从 1 开始) + self.q = None # 接收 bar 的队列 - # market time offsets + # 市场时间偏移 self._mktoffset = None self._mktoff1 = None self._mktoffdiff = None if not self.store.connected(): - # Not connected -> go away + # 未连接,直接退出 self.put_notification(self.DISCONNECTED) self._state = self._ST_NOTFOUND return self.put_notification(self.CONNECTED) - # get real contract details with real conId (contractId) - self.qrt = queue.Queue() # to await a ping + # 获取真实 symbol 信息 + self.qrt = queue.Queue() # 等待 ping self.store._rtdata(self, self._dataname) symfound = self.qrt.get() if not symfound: - # Kill any further action and signal it + # 停止后续动作并发出通知 self.put_notification(self.NOTSUBSCRIBED) self.put_notification(self.DISCONNECTED) self._state = self._ST_NOTFOUND return if self.replaying: - # In this case don't request the final - # timeframe from vc, but the original that has to be replayed + # replaying 时不要向 VC 请求最终 timeframe,而是请求需要 replay 的原始周期 self._tf, self._comp = self.p.timeframe, self.p.compression else: - # Else (even if resampling) pass the final timeframe which may - # been modified by a resampling filter + # 其他情况(包括 resampling)传递可能已被 filter 修改的最终 timeframe self._tf, self._comp = self._timeframe, self._compression, self._ticking = self.store._ticking(self._tf) self._syminfo = syminfo = self.store._symboldata(self._dataname) - # For most markets: - # mktoffset == mktoff1 and substracting this value from reported times - # is enough to report the "market time". Visual Chart changes this from - # a value X to 0 if the appropriate setting in the GUI is changed to - # change display of time from local <-> market + # 对大多数市场: + # mktoffset == mktoff1,从返回时间中减去该值即可得到“市场时间”。 + # 如果在 Visual Chart GUI 中切换本地时间/市场时间显示,Visual Chart 会把该值 + # 从 X 改为 0。 # - # But some markets (at least 096XXX) that theoretically live in - # Europe/London seem to be displaced 1 hour to the west and an extra - # hour is needed. - # These markets do also need "usetimezoned" True to actually display - # the market time, because this is done internally using the - # definitions in TZOUTS - - # Record and calculate market offsets + # 但某些市场(至少 096XXX)理论上属于 Europe/London,实际看起来向西偏移了 + # 1 小时,因此需要额外补 1 小时。这些市场也需要 usetimezones=True 才能显示 + # 用户预期的市场时间,因为内部会使用 TZOUTS 定义。 + + # 记录并计算市场时间偏移 self._mktoffset = timedelta(seconds=syminfo.TimeOffset) - # Add millisecond to pusth HH:MM:59.999 -> 00.000 unless ticks + # 非 tick 数据增加 millisecond,把 HH:MM:59.999 推到下一分钟 00.000 if self.p.millisecond and not self._ticking: self._mktoffset -= self.MILLISECOND self._mktoff1 = self._mktoffset if self._mktcode in self._EXTRA_TIMEOFFSET: - # These codes live theoretically in - # (UTC+00:00) Dublin, Edinburgh, Lisbon, London which is - # 'Europe/London' - # But all experiments show the times to be displaced 1 hour to - # the west and hence the extra 3600 seconds + # 这些代码理论上位于 (UTC+00:00) Dublin/Edinburgh/Lisbon/London, + # 即 Europe/London;但实验显示时间向西偏移 1 小时,因此额外减 3600 秒 self._mktoffset -= timedelta(seconds=3600) self._mktoffdiff = self._mktoffset - self._mktoff1 @@ -349,7 +311,7 @@ def start(self): if self._state == self._ST_START: self.put_notification(self.DELAYED) - # Now request the data and get a comms queue for it + # 请求数据并获取通信队列 self.q = self.store._directdata( self, self._dataname, @@ -360,13 +322,13 @@ def start(self): self._state = self._ST_FEEDING def stop(self): - '''Stops and tells the store to stop''' + '''停止数据源,并通知 store 停止。''' super(VCData, self).stop() if self.q: self.store._canceldirectdata(self.q) def _setserie(self, serie): - # Accepts a serie (COM Object) to use in ping events + # 接收 serie(COM 对象),用于 ping 事件 self._serie = serie def haslivedata(self): @@ -374,22 +336,22 @@ def haslivedata(self): def _load(self): if self._state == self._ST_NOTFOUND: - return False # nothing can be done + return False # 无法继续处理 while True: try: - # tmout <> 0 only if resampling/replaying, else no waking up + # 只有 resampling/replaying 时 tmout 才不为 0,否则不唤醒 tmout = self._qcheck * bool(self.resampling) msg = self.q.get(timeout=tmout) except queue.Empty: return None if msg is None: - return False # end of stream + return False # 数据流结束 if msg == self.store._RT_SHUTDOWN: self.put_notification(self.DISCONNECTED) - return False # VC has exited + return False # VC 已退出 if msg == self.store._RT_DISCONNECTED: self.put_notification(self.CONNBROKEN) @@ -414,10 +376,10 @@ def _load(self): self.put_notification(self.UNKNOWN, msg) continue - # it must be a bar + # 走到这里时 msg 必然是 bar bar = msg - # Put the tick into the bar + # 把 tick 写入 bar self.lines.open[0] = bar.Open self.lines.high[0] = bar.High self.lines.low[0] = bar.Low @@ -425,122 +387,117 @@ def _load(self): self.lines.volume[0] = bar.Volume self.lines.openinterest[0] = bar.OpenInterest - # Convert time to "market" time (096 exception) + # 转换为“市场时间”(含 096 特例) dt = self.NULLDATE + timedelta(days=bar.Date) - self._mktoffset self.lines.datetime[0] = date2num(dt) return True # - # DS Events + # DS 事件 # def _getpingtmout(self): - '''Returns the actual ping timeout for PumpEvents to wake up and call - ping, which will check if the not yet delivered bar can be - delivered. The bar may be stalled because vc awaits a new tick and - during low negotiation hour this can take several seconds after the - actual expected delivery time''' + '''返回 PumpEvents 唤醒并调用 ping 所需的实际超时时间。 + + ping 会检查尚未交付的 bar 是否已经可以交付。VC 可能因为等待新 tick 而卡住 + 当前 bar;在交易清淡时,这可能比预期交付时间晚几秒。 + ''' if self._ticking: - return -1 # no timeout + return -1 # 无超时 return self._pingtmout def OnNewDataSerieBar(self, DataSerie, forcepush=False): - # Processes the COM Event (also called directly when 1st creating the - # data serie + # 处理 COM 事件;首次创建 data serie 时也会直接调用 ssize = DataSerie.Size if ssize - self.idx > 1: - # More than 1 bar on-board -> delay in place + # 队列中超过 1 个 bar,说明存在延迟 if self._laststatus != self.DELAYED: self.q.put(self.store._RT_DELAYED) - # return everything if original tf is ticks or force pushing + # 原始 timeframe 为 ticks 或强制推送时,返回全部内容 ssize += forcepush or self._ticking for idx in range(self.idx, ssize): bar = DataSerie.GetBarValues(idx) self.q.put(bar) if not forcepush and not self._ticking and ssize: - # A bar has been left in place - dtnow = datetime.now() - self._TOFFSET # adjust local time + # 仍有一个 bar 留在当前位置 + dtnow = datetime.now() - self._TOFFSET # 修正本地时间 bar = DataSerie.GetBarValues(ssize) dt = self.NULLDATE + timedelta(days=bar.Date) - self._mktoffdiff if dtnow < dt: - # A bar is there, not deliverable yet - LIVE + # bar 已存在但尚未到可交付时间,仍为 LIVE if self._laststatus != self.LIVE: self.q.put(self.store._RT_LIVE) - # Adjust ping timeout to the bar boundary (plus mini leeway) + # 把 ping 超时调整到 bar 边界,并增加少量余量 self._pingtmout = (dt - dtnow).total_seconds() + 0.5 else: - self._pingtmout = self.PING_TIMEOUT # no bar left, long pause - self.q.put(bar) # push bar and update index - ssize += 1 # pushed last one out + self._pingtmout = self.PING_TIMEOUT # 没有 bar 剩余,长暂停 + self.q.put(bar) # 推送 bar 并更新索引 + ssize += 1 # 已推出最后一个 bar - # Write down the last processed bar + # 记录最后处理过的 bar self.idx = max(1, ssize) def ping(self): ssize = self._serie.Size if self.idx > ssize: - return # no bar available + return # 没有可用 bar if self._laststatus == self.CONNBROKEN: self._pingtmout = self.PING_TIMEOUT - return # do not push during disconnection + return # 断线期间不推送 dtnow = datetime.now() - self._TOFFSET - # CHECK: there should be a maximum of 1 bar when pinging - # In any case the algorithm doesn't hurt - for idx in range(self.idx, ssize + 1): # reach ssize + # CHECK: ping 时最多应该只有 1 个 bar;即便更多,该算法也不会造成伤害 + for idx in range(self.idx, ssize + 1): # 到达 ssize bar = self._serie.GetBarValues(self.idx) # dt = (self.NULLDATE + timedelta(days=bar.Date) + self._mktoff1) dt = self.NULLDATE + timedelta(days=bar.Date) - self._mktoffdiff if dtnow < dt: self._pingtmout = (dt - dtnow).total_seconds() + 0.5 - break # cannot deliver anything + break # 还不能交付 - # Adjust ping timeout to the bar boundary (plus mini leeway) - self._pingtmout = self.PING_TIMEOUT # no bar, nothing to check - self.q.put(bar) # push bar and update index + # 把 ping 超时调整到 bar 边界,并增加少量余量 + self._pingtmout = self.PING_TIMEOUT # 没有 bar,无需检查 + self.q.put(bar) # 推送 bar 并更新索引 self.idx += 1 # - # RTEvents + # RT 事件 # - # Can be used on a per data basis to check the connection status + # 可按单个 data 检查连接状态 if False: def OnInternalEvent(self, p1, p2, p3): - if p1 != 1: # Apparently "Connection Event" + if p1 != 1: # 看起来是 "Connection Event" return if p2 == self.lastconn: - return # do not notify twice + return # 不重复通知 - self.lastconn = p2 # keep new notification code + self.lastconn = p2 # 保存新的 notification code # p2 should be 0 (disconn), 1 (conn) self.store._vcrt_connection(self.store._RT_BASEMSG - p2) def OnNewTicks(self, ArrayTicks): - # Process the COM Event for New Ticks. This is only used temporarily - # for 2 purposes + # 处理 New Ticks 的 COM 事件。这里临时用于两个目的: # - # 1. If tick.Field == Field_Description is returned, it can be checked - # if the requested symbol has been found or not (tick.Date == 0 -> not - # found). tick.Text has 'Not Found', but this is more likely to change - # Once Field_Description has been seen, the 2nd stage takes place + # 1. 如果返回 tick.Field == Field_Description,就可以检查请求的 symbol 是否 + # 已找到(tick.Date == 0 表示未找到)。tick.Text 也有 'Not Found',但更容易 + # 变化。看到 Field_Description 后进入第 2 阶段。 # - # 2. When a tick.Field == Field_Time is seen and tick.TickIndex == 0, - # the 1st tick of a second is seen and the tick.Date value can be used - # to calculate a time offset to the feed server. This is later used to - # check if a bar is due delivery or not + # 2. 当看到 tick.Field == Field_Time 且 tick.TickIndex == 0 时,表示看到了 + # 某一秒的第一个 tick,可用 tick.Date 计算到 feed server 的时间偏移。后续用 + # 它判断 bar 是否到了交付时间。 # - # After this the reception of ticks is cancelled + # 完成后会取消 tick 接收 aticks = ArrayTicks[0] # self.debug_ticks(aticks) @@ -562,19 +519,17 @@ def OnNewTicks(self, ArrayTicks): return if tick.TickIndex == 0 and self._mktoff1 is not None: - # Adjust the tick time using the mktoffset (with the 096 excep) + # 使用 mktoffset 修正 tick 时间(含 096 特例) dttick = (self.NULLDATE + timedelta(days=tick.Date) + self._mktoff1) self._TOFFSET = datetime.now() - dttick if self._mktcode in self._EXTRA_TIMEOFFSET: - # These codes live theoretically in (UTC+00:00) Dublin, - # Edinburgh, Lisbon, London which is 'Europe/London' - # But all experiments show the times to be displaced 1 - # hour to the west and hence the extra 3600 seconds + # 这些代码理论上位于 (UTC+00:00) Dublin/Edinburgh/Lisbon/London, + # 即 Europe/London;但实验显示时间向西偏移 1 小时,因此额外减 3600 秒 self._TOFFSET -= timedelta(seconds=3600) - # Cancel ticks + # 取消 tick 接收 self._vcrt.CancelSymbolFeed(self._dataname, False) def debug_ticks(self, ticks): diff --git a/backtrader/feeds/vchart.py b/backtrader/feeds/vchart.py index 9f042c718..e1452b66b 100644 --- a/backtrader/feeds/vchart.py +++ b/backtrader/feeds/vchart.py @@ -32,35 +32,40 @@ class VChartData(feed.DataBase): ''' - Support for `Visual Chart `_ binary on-disk files for - both daily and intradaily formats. + 支持 `Visual Chart `_ 的本地二进制文件,包括日线和日内 + 数据格式。 - Note: + Args: + dataname: 文件路径,或已经打开的类文件对象。传入类文件对象时,使用 + ``timeframe`` 参数判断实际 timeframe;传入路径时,优先根据扩展名 + 判断,``.fd`` 表示日线,``.min`` 表示日内数据。 - - ``dataname``: to file or open file-like object + Returns: + VChartData: 可加入 Cerebro 的 Visual Chart 数据源实例。 - If a file-like object is passed, the ``timeframe`` parameter will be - used to determine which is the actual timeframe. + --- + 交互界面使用示范: - Else the file extension (``.fd`` for daily and ``.min`` for intraday) - will be used. + >>> data = VChartData(dataname='010015ES.fd') + >>> data.p.dataname + '010015ES.fd' ''' def start(self): super(VChartData, self).start() - # Not yet known if a extension is needed + # 先记录扩展名,后面根据 dataname 和 timeframe 决定是否补全 self.ext = '' if not hasattr(self.p.dataname, 'read'): - # assume is a string because it has no write method + # 没有 read 方法时按字符串路径处理 if self.p.dataname.endswith('.fd'): self.p.timeframe = TimeFrame.Days elif self.p.dataname.endswith('.min'): self.p.timeframe = TimeFrame.Minutes else: - # Neither fd nor min ... just the code, assign extension + # 没有 fd/min 扩展名时,根据 timeframe 自动补扩展名 if self.p.timeframe == TimeFrame.Days: self.ext = '.fd' else: @@ -77,11 +82,11 @@ def start(self): self.f = None if hasattr(self.p.dataname, 'read'): - # A file has been passed in (ex: from a GUI) + # 已经传入打开的文件对象,例如来自 GUI 的文件选择器 self.f = self.p.dataname else: dataname = self.p.dataname + self.ext - # Let an exception propagate + # 打不开文件时让异常向上传递,调用方能看到真实原因 self.f = open(dataname, 'rb') def stop(self): @@ -93,21 +98,21 @@ def _load(self): if self.f is None: return False - # Let an exception propagate to let the caller know + # 读取失败时让异常向上传递,调用方能看到真实原因 bardata = self.f.read(self.barsize) if not bardata: return False bdata = struct.unpack(self.barfmt, bardata) - # Years are stored as if they had 500 days + # 年份按“每年 500 天”的编码方式存储 y, md = divmod(bdata[0], 500) - # Months are stored as if they had 32 days + # 月份按“每月 32 天”的编码方式存储 m, d = divmod(md, 32) dt = datetime.datetime(y, m, d) - if self.dtsize > 1: # Minute Bars - # Daily Time is stored in seconds + if self.dtsize > 1: # 分钟 bar + # 日内时间以秒数存储 hhmm, ss = divmod(bdata[1], 60) hh, mm = divmod(hhmm, 60) dt = dt.replace(hour=hh, minute=mm, second=ss) diff --git a/backtrader/feeds/vchartcsv.py b/backtrader/feeds/vchartcsv.py index d5dbdf345..00d80217c 100644 --- a/backtrader/feeds/vchartcsv.py +++ b/backtrader/feeds/vchartcsv.py @@ -30,11 +30,20 @@ class VChartCSVData(feed.CSVDataBase): ''' - Parses a `VisualChart `_ CSV exported file. + 解析 `VisualChart `_ 导出的 CSV 文件。 - Specific parameters (or specific meaning): + Args: + dataname: 要解析的文件名,或已经打开的类文件对象。 - - ``dataname``: The filename to parse or a file-like object + Returns: + VChartCSVData: 可加入 Cerebro 的 VisualChart CSV 数据源实例。 + + --- + 交互界面使用示范: + + >>> data = VChartCSVData(dataname='visualchart.csv') + >>> data.p.dataname + 'visualchart.csv' ''' vctframes = dict( @@ -46,11 +55,11 @@ class VChartCSVData(feed.CSVDataBase): def _loadline(self, linetokens): itokens = iter(linetokens) - ticker = next(itokens) # skip ticker name + ticker = next(itokens) # 跳过 ticker 名称 if not self._name: self._name = ticker - # day/intraday indication + # 日线/日内数据标记 timeframe = next(itokens) self._timeframe = self.vctframes[timeframe] @@ -60,11 +69,11 @@ def _loadline(self, linetokens): tmtxt = next(itokens) if timeframe == 'I': - # use the provided time + # 日内数据使用文件提供的时间 hh, mmss = divmod(int(tmtxt), 10000) mm, ss = divmod(mmss, 100) else: - # put it at the end of the session parameter + # 日线及以上周期放到 sessionend 指定的收盘时间 hh = self.p.sessionend.hour mm = self.p.sessionend.minute ss = self.p.sessionend.second diff --git a/backtrader/feeds/vchartfile.py b/backtrader/feeds/vchartfile.py index 1e7b84ec6..1b54f4551 100644 --- a/backtrader/feeds/vchartfile.py +++ b/backtrader/feeds/vchartfile.py @@ -26,28 +26,37 @@ import os.path import backtrader as bt -from backtrader import date2num # avoid dict lookups +from backtrader import date2num # 避免字典查找 class MetaVChartFile(bt.DataBase.__class__): def __init__(cls, name, bases, dct): - '''Class has already been created ... register''' - # Initialize the class + '''类已经创建完成,随后把它注册到对应 store。''' + # 初始化类对象 super(MetaVChartFile, cls).__init__(name, bases, dct) - # Register with the store + # 注册到 store,供 VChartFile store 找到实际 DataCls bt.stores.VChartFile.DataCls = cls class VChartFile(bt.with_metaclass(MetaVChartFile, bt.DataBase)): ''' - Support for `Visual Chart `_ binary on-disk files for - both daily and intradaily formats. + 支持 `Visual Chart `_ 的本地二进制文件,包括日线和日内 + 数据格式。 - Note: + Args: + dataname: Visual Chart 显示的市场代码。例如 ``015ES`` 表示 EuroStoxx + 50 连续期货。 - - ``dataname``: Market code displayed by Visual Chart. Example: 015ES for - EuroStoxx 50 continuous future + Returns: + VChartFile: 可加入 Cerebro 的 Visual Chart 文件数据源实例。 + + --- + 交互界面使用示范: + + >>> data = VChartFile(dataname='015ES') + >>> data.p.dataname + '015ES' ''' def start(self): @@ -58,10 +67,10 @@ def start(self): self._store.start(data=self) - # Choose extension and extraction/calculation parameters + # 根据 timeframe 选择扩展名和解析参数 if self.p.timeframe < bt.TimeFrame.Minutes: - ext = '.tck' # seconds will still need resampling - # FIXME: find reference to tick counter for format + ext = '.tck' # 秒级数据仍需要 resampling + # FIXME: 找到 tick 计数器格式的参考资料 elif self.p.timeframe < bt.TimeFrame.Days: ext = '.min' self._dtsize = 2 @@ -73,10 +82,10 @@ def start(self): self._dtsize = 1 self._barfmt = 'IffffII' - # Construct full path + # 拼出完整文件路径 basepath = self._store.get_datapath() - # Example: 01 + 0 + 015ES + .fd -> 010015ES.fd + # 例如:01 + 0 + 015ES + .fd -> 010015ES.fd dataname = '01' + '0' + self.p.dataname + ext # 015ES -> 0 + 015 -> 0015 mktcode = '0' + self.p.dataname[0:3] @@ -95,17 +104,17 @@ def stop(self): def _load(self): if self.f is None: - return False # cannot load more + return False # 没有更多数据可加载 try: bardata = self.f.read(self._barsize) except IOError: - self.f = None # cannot return, nullify file - return False # cannot load more + self.f = None # 无法继续读取,清空文件对象 + return False # 没有更多数据可加载 if not bardata or len(bardata) < self._barsize: - self.f = None # cannot return, nullify file - return False # cannot load more + self.f = None # 无法继续读取,清空文件对象 + return False # 没有更多数据可加载 try: bdata = unpack(self._barfmt, bardata) @@ -113,23 +122,23 @@ def _load(self): self.f = None return False - # First Date - y, md = divmod(bdata[0], 500) # Years stored as if they had 500 days - m, d = divmod(md, 32) # Months stored as if they had 32 days + # 先解析日期 + y, md = divmod(bdata[0], 500) # 年份按“每年 500 天”的编码方式存储 + m, d = divmod(md, 32) # 月份按“每月 32 天”的编码方式存储 dt = datetime(y, m, d) - # Time - if self._dtsize > 1: # Minute Bars - # Daily Time is stored in seconds + # 再解析时间 + if self._dtsize > 1: # 分钟 bar + # 日内时间以秒数存储 hhmm, ss = divmod(bdata[1], 60) hh, mm = divmod(hhmm, 60) dt = dt.replace(hour=hh, minute=mm, second=ss) - else: # Daily Bars + else: # 日线 bar dt = datetime.combine(dt, self.p.sessionend) - self.lines.datetime[0] = date2num(dt) # Store time + self.lines.datetime[0] = date2num(dt) # 存储时间 - # Get the rest of the fields + # 读取剩余字段 o, h, l, c, v, oi = bdata[self._dtsize:] self.lines.open[0] = o self.lines.high[0] = h @@ -138,4 +147,4 @@ def _load(self): self.lines.volume[0] = v self.lines.openinterest[0] = oi - return True # a bar has been successfully loaded + return True # 成功加载了一个 bar diff --git a/backtrader/feeds/yahoo.py b/backtrader/feeds/yahoo.py index d9a05212a..9ed22c16a 100644 --- a/backtrader/feeds/yahoo.py +++ b/backtrader/feeds/yahoo.py @@ -36,47 +36,30 @@ class YahooFinanceCSVData(feed.CSVDataBase): ''' - Parses pre-downloaded Yahoo CSV Data Feeds (or locally generated if they - comply to the Yahoo format) - - Specific parameters: - - - ``dataname``: The filename to parse or a file-like object - - - ``reverse`` (default: ``False``) - - It is assumed that locally stored files have already been reversed - during the download process - - - ``adjclose`` (default: ``True``) - - Whether to use the dividend/split adjusted close and adjust all - values according to it. - - - ``adjvolume`` (default: ``True``) - - Do also adjust ``volume`` if ``adjclose`` is also ``True`` - - - ``round`` (default: ``True``) - - Whether to round the values to a specific number of decimals after - having adjusted the close - - - ``roundvolume`` (default: ``0``) - - Round the resulting volume to the given number of decimals after having - adjusted it - - - ``decimals`` (default: ``2``) - - Number of decimals to round to - - - ``swapcloses`` (default: ``False``) - - [2018-11-16] It would seem that the order of *close* and *adjusted - close* is now fixed. The parameter is retained, in case the need to - swap the columns again arose. - + 解析已经下载好的 Yahoo CSV data feed;本地生成但符合 Yahoo 格式的 CSV + 也可以使用。 + + Args: + dataname: 要解析的文件名,或已经打开的类文件对象。 + reverse: 是否反转本地文件顺序,默认 ``False``。通常认为本地保存的文件在下载 + 阶段已经处理过顺序。 + adjclose: 是否使用经过分红/拆股调整的 close,并据此调整全部价格字段。 + adjvolume: ``adjclose`` 为 ``True`` 时,是否同步调整 ``volume``。 + round: 是否在调整 close 后按指定小数位四舍五入。 + roundvolume: 调整 volume 后保留的小数位数。 + decimals: 价格字段保留的小数位数。 + swapcloses: 是否交换 close 与 adjusted close。该参数保留用于兼容 Yahoo + 可能再次调整列顺序的情况。 + + Returns: + YahooFinanceCSVData: 可加入 Cerebro 的 Yahoo CSV 数据源实例。 + + --- + 交互界面使用示范: + + >>> data = YahooFinanceCSVData(dataname='YHOO.csv', reverse=True) + >>> data.p.reverse + True ''' lines = ('adjclose',) @@ -96,7 +79,7 @@ def start(self): if not self.params.reverse: return - # Yahoo sends data in reverse order and the file is still unreversed + # Yahoo 发送的数据是倒序,而文件还没有反转 dq = collections.deque() for line in self.f: dq.appendleft(line) @@ -113,15 +96,15 @@ def _loadline(self, linetokens): for tok in linetokens[1:]: if tok == 'null': nullseen = True - linetokens = self._getnextline() # refetch tokens + linetokens = self._getnextline() # 重新获取 tokens if not linetokens: - return False # cannot fetch, go away + return False # 无法继续获取,结束加载 - # out of for to carry on wiwth while True logic + # 跳出 for,继续 while True 的 null 检查逻辑 break if not nullseen: - break # can proceed + break # 可以继续解析 i = itertools.count(0) @@ -136,26 +119,25 @@ def _loadline(self, linetokens): c = float(linetokens[next(i)]) self.lines.openinterest[0] = 0.0 - # 2018-11-16 ... Adjusted Close seems to always be delivered after - # the close and before the volume columns + # 2018-11-16 起,Adjusted Close 看起来总是在 close 之后、volume 之前 adjustedclose = float(linetokens[next(i)]) try: v = float(linetokens[next(i)]) - except: # cover the case in which volume is "null" + except: # 覆盖 volume 为 "null" 的情况 v = 0.0 - if self.p.swapcloses: # swap closing prices if requested + if self.p.swapcloses: # 按需交换 close 和 adjusted close c, adjustedclose = adjustedclose, c adjfactor = c / adjustedclose - # in v7 "adjusted prices" seem to be given, scale back for non adj + # v7 中似乎直接给出 adjusted prices,需要按比例还原未调整价格 if self.params.adjclose: o /= adjfactor h /= adjfactor l /= adjfactor c = adjustedclose - # If the price goes down, volume must go up and viceversa + # 价格向下调整时,volume 要向上调整,反之亦然 if self.p.adjvolume: v *= adjfactor @@ -180,9 +162,20 @@ def _loadline(self, linetokens): class YahooLegacyCSV(YahooFinanceCSVData): ''' - This is intended to load files which were downloaded before Yahoo - discontinued the original service in May-2017 + 用于加载 Yahoo 在 2017 年 5 月停用原始服务前下载的旧 CSV 文件。 + + Args: + version: 旧版兼容标记,默认空字符串。 + + Returns: + YahooLegacyCSV: 可加入 Cerebro 的旧版 Yahoo CSV 数据源实例。 + --- + 交互界面使用示范: + + >>> data = YahooLegacyCSV(dataname='legacy.csv') + >>> data.p.version + '' ''' params = ( ('version', ''), @@ -195,50 +188,28 @@ class YahooFinanceCSV(feed.CSVFeedBase): class YahooFinanceData(YahooFinanceCSVData): ''' - Executes a direct download of data from Yahoo servers for the given time - range. - - Specific parameters (or specific meaning): - - - ``dataname`` - - The ticker to download ('YHOO' for Yahoo own stock quotes) - - - ``proxies`` - - A dict indicating which proxy to go through for the download as in - {'http': 'http://myproxy.com'} or {'http': 'http://127.0.0.1:8080'} - - - ``period`` - - The timeframe to download data in. Pass 'w' for weekly and 'm' for - monthly. - - - ``reverse`` - - [2018-11-16] The latest incarnation of Yahoo online downloads returns - the data in the proper order. The default value of ``reverse`` for the - online download is therefore set to ``False`` - - - ``adjclose`` - - Whether to use the dividend/split adjusted close and adjust all values - according to it. - - - ``urlhist`` - - The url of the historical quotes in Yahoo Finance used to gather a - ``crumb`` authorization cookie for the download - - - ``urldown`` - - The url of the actual download server - - - ``retries`` - - Number of times (each) to try to get a ``crumb`` cookie and download - the data - + 按指定时间范围直接从 Yahoo 服务器下载数据。 + + Args: + dataname: 要下载的 ticker,例如 Yahoo 自身股票报价曾使用的 ``'YHOO'``。 + proxies: 下载时使用的代理字典,例如 + ``{'http': 'http://127.0.0.1:8080'}``。 + period: 下载周期,``'w'`` 表示周线,``'m'`` 表示月线。 + reverse: 在线下载默认返回正确顺序,因此默认为 ``False``。 + adjclose: 是否使用经过分红/拆股调整的 close,并据此调整全部价格字段。 + urlhist: Yahoo Finance 历史报价页面 URL,用于获取下载需要的 ``crumb`` cookie。 + urldown: 实际下载服务的 URL。 + retries: 获取 ``crumb`` 和下载数据各自最多重试的次数。 + + Returns: + YahooFinanceData: 可加入 Cerebro 的 Yahoo 在线数据源实例。 + + --- + 交互界面使用示范: + + >>> data = YahooFinanceData(dataname='YHOO') + >>> data.p.retries + 3 ''' params = ( @@ -269,7 +240,7 @@ def start_v7(self): crumb = None sess = requests.Session() sess.headers['User-Agent'] = 'backtrader' - for i in range(self.p.retries + 1): # at least once + for i in range(self.p.retries + 1): # 至少尝试一次 resp = sess.get(url, **sesskwargs) if resp.status_code != requests.codes.ok: continue @@ -302,7 +273,7 @@ def start_v7(self): # urldown/ticker?period1=posix1&period2=posix2&interval=1d&events=history&crumb=crumb - # Try to download + # 尝试下载 urld = '{}/{}'.format(self.p.urldown, self.p.dataname) urlargs = [] @@ -327,23 +298,23 @@ def start_v7(self): urld = '{}?{}'.format(urld, '&'.join(urlargs)) f = None - for i in range(self.p.retries + 1): # at least once + for i in range(self.p.retries + 1): # 至少尝试一次 resp = sess.get(urld, **sesskwargs) if resp.status_code != requests.codes.ok: continue ctype = resp.headers['Content-Type'] - # Cover as many text types as possible for Yahoo changes + # 尽量覆盖 Yahoo 可能返回的各种文本类型 if not ctype.startswith('text/'): self.error = 'Wrong content type: %s' % ctype - continue # HTML returned? wrong url? + continue # 返回了 HTML,通常表示 URL 不正确 - # buffer everything from the socket into a local buffer + # 把 socket 返回内容全部缓冲到本地 try: # r.encoding = 'UTF-8' f = io.StringIO(resp.text, newline=None) except Exception: - continue # try again if possible + continue # 如果还能重试,则继续尝试 break @@ -352,7 +323,7 @@ def start_v7(self): def start(self): self.start_v7() - # Prepared a "path" file - CSV Parser can take over + # 已准备好类文件对象,交给 CSV parser 接管 super(YahooFinanceData, self).start() diff --git a/backtrader/fillers.py b/backtrader/fillers.py index 3bd298f88..c3f14516c 100644 --- a/backtrader/fillers.py +++ b/backtrader/fillers.py @@ -28,68 +28,120 @@ class FixedSize(with_metaclass(MetaParams, object)): - '''Returns the execution size for a given order using a *percentage* of the - volume in a bar. - - This percentage is set with the parameter ``perc`` - - Params: - - - ``size`` (default: ``None``) maximum size to be executed. The actual - volume of the bar at execution time is also a limit if smaller than the - size - - If the value of this parameter evaluates to False, the entire volume - of the bar will be used to match the order + '''按固定上限返回给定 order 的 execution size。 + + Args: + size: 最大可执行 size。实际执行时的 bar volume 也是限制;如果 bar + volume 更小,则使用更小值。 + + 如果该参数的值为 False,则使用 bar 的全部 volume 来匹配 order。 + + --- + 交互示例: + + >>> class Line: + ... def __getitem__(self, ago): + ... return 100 + >>> class Data: + ... volume = Line() + >>> class Executed: + ... remsize = 25 + >>> class Order: + ... data = Data() + ... executed = Executed() + >>> FixedSize(size=10)(Order(), price=100.0, ago=0) + 10 ''' params = (('size', None),) def __call__(self, order, price, ago): + '''计算可执行 size。 + + Args: + order: 当前 order,需提供 ``data.volume`` 和 ``executed.remsize``。 + price (float): 当前 execution price。 + ago (int): 访问 data line 时使用的相对位置。 + + Returns: + int: 当前可执行 size。 + ''' size = self.p.size or MAXINT return min((order.data.volume[ago], abs(order.executed.remsize), size)) class FixedBarPerc(with_metaclass(MetaParams, object)): - '''Returns the execution size for a given order using a *percentage* of the - volume in a bar. - - This percentage is set with the parameter ``perc`` - - Params: - - - ``perc`` (default: ``100.0``) (valied values: ``0.0 - 100.0``) - - Percentage of the volume bar to use to execute an order + '''使用 bar volume 的固定百分比返回给定 order 的 execution size。 + + Args: + perc (float): 用于执行 order 的 bar volume 百分比,合法值为 + ``0.0 - 100.0``。 + + --- + 交互示例: + + >>> class Line: + ... def __getitem__(self, ago): + ... return 100 + >>> class Data: + ... volume = Line() + >>> class Executed: + ... remsize = 80 + >>> class Order: + ... data = Data() + ... executed = Executed() + >>> FixedBarPerc(perc=50.0)(Order(), price=100.0, ago=0) + 50.0 ''' params = (('perc', 100.0),) def __call__(self, order, price, ago): - # Get the volume and scale it to the requested perc + '''计算按 bar volume 百分比限制后的可执行 size。 + + Args: + order: 当前 order,需提供 ``data.volume`` 和 ``executed.remsize``。 + price (float): 当前 execution price。 + ago (int): 访问 data line 时使用的相对位置。 + + Returns: + float: 当前可执行 size。 + ''' + # 获取 volume,并按请求的 perc 缩放 maxsize = (order.data.volume[ago] * self.p.perc) // 100 - # Return the maximum possible executed volume + # 返回最大可能执行 volume return min(maxsize, abs(order.executed.remsize)) class BarPointPerc(with_metaclass(MetaParams, object)): - '''Returns the execution size for a given order. The volume will be - distributed uniformly in the range *high*-*low* using ``minmov`` to - partition. - - From the allocated volume for the given price, the ``perc`` percentage will - be used - - Params: - - - ``minmov`` (default: ``0.01``) - - Minimum price movement. Used to partition the range *high*-*low* to - proportionally distribute the volume amongst possible prices - - - ``perc`` (default: ``100.0``) (valied values: ``0.0 - 100.0``) - - Percentage of the volume allocated to the order execution price to use - for matching - + '''返回给定 order 的 execution size,并按 price 区间分配 bar volume。 + + volume 会在 *high*-*low* 区间内用 ``minmov`` 分区并均匀分布。给定 price + 分到的 volume 会再按 ``perc`` 百分比用于匹配。 + + Args: + minmov (float): 最小 price movement。用于将 *high*-*low* 区间分区, + 以便在可能 price 之间按比例分配 volume。 + perc (float): 分配给 order execution price 的 volume 中用于匹配的 + 百分比,合法值为 ``0.0 - 100.0``。 + + --- + 交互示例: + + >>> class Line: + ... def __init__(self, value): + ... self.value = value + ... def __getitem__(self, ago): + ... return self.value + >>> class Data: + ... high = Line(101.0) + ... low = Line(100.0) + ... volume = Line(100) + >>> class Executed: + ... remsize = 80 + >>> class Order: + ... data = Data() + ... executed = Executed() + >>> BarPointPerc(minmov=0.5, perc=50.0)(Order(), price=100.5, ago=0) + 16.0 ''' params = ( ('minmov', None), @@ -97,15 +149,26 @@ class BarPointPerc(with_metaclass(MetaParams, object)): ) def __call__(self, order, price, ago): + '''计算按 price 分区和百分比限制后的可执行 size。 + + Args: + order: 当前 order,需提供 ``data.high``、``data.low``、 + ``data.volume`` 和 ``executed.remsize``。 + price (float): 当前 execution price。 + ago (int): 访问 data line 时使用的相对位置。 + + Returns: + float: 当前可执行 size。 + ''' data = order.data minmov = self.p.minmov parts = 1 if minmov: - # high - low + minmov to account for open ended minus op + # high - low + minmov 用于处理开区间减法 parts = (data.high[ago] - data.low[ago] + minmov) // minmov alloc_vol = ((data.volume[ago] / parts) * self.p.perc) // 100.0 - # return max possible executable volume + # 返回最大可能执行 volume return min(alloc_vol, abs(order.executed.remsize)) diff --git a/backtrader/filters/bsplitter.py b/backtrader/filters/bsplitter.py index 997aa2a51..da25cf8ad 100644 --- a/backtrader/filters/bsplitter.py +++ b/backtrader/filters/bsplitter.py @@ -27,36 +27,36 @@ class DaySplitter_Close(bt.with_metaclass(bt.MetaParams, object)): - ''' - Splits a daily bar in two parts simulating 2 ticks which will be used to - replay the data: - - - First tick: ``OHLX`` + '''将日线 bar 拆成两部分,用两个 tick 模拟 replay data 的 filter。 - The ``Close`` will be replaced by the *average* of ``Open``, ``High`` - and ``Low`` + 拆分结果: - The session opening time is used for this tick + - 第 1 个 tick: ``OHLX`` - and + ``Close`` 会被替换为 ``Open``、``High`` 和 ``Low`` 的平均值,并使用 + session opening time。 - - Second tick: ``CCCC`` + - 第 2 个 tick: ``CCCC`` - The ``Close`` price will be used for the four components of the price + 使用 ``Close`` 价格填充四个 price 组件,并使用 session closing time。 - The session closing time is used for this tick + Args: + closevol (float): 分配给 closing tick 的 volume 比例,默认 ``0.5``。 + 取值按 0.0 到 1.0 的绝对比例理解,剩余 volume 分配给 ``OHLX`` tick。 - The volume will be split amongst the 2 ticks using the parameters: + Returns: + bool: ``__call__`` 返回 ``False``,让初始 tick 可继续从 stack 中处理。 - - ``closevol`` (default: ``0.5``) The value indicate which percentage, in - absolute terms from 0.0 to 1.0, has to be assigned to the *closing* - tick. The rest will be assigned to the ``OHLX`` tick. + **该 filter 设计为配合** ``cerebro.replaydata`` **使用**。 - **This filter is meant to be used together with** ``cerebro.replaydata`` + --- + >>> import backtrader as bt + >>> data = bt.feeds.GenericCSVData(dataname='daily.csv') + >>> data.addfilter(DaySplitter_Close, closevol=0.5) ''' params = ( - ('closevol', 0.5), # 0 -> 1 amount of volume to keep for close + ('closevol', 0.5), # 保留给 close 的 volume 比例,范围 0 -> 1 ) # replaying = True @@ -65,47 +65,47 @@ def __init__(self, data): self.lastdt = None def __call__(self, data): - # Make a copy of the new bar and remove it from stream - datadt = data.datetime.date() # keep the date + # 复制新 bar,并从 stream 中移除 + datadt = data.datetime.date() # 保留日期 if self.lastdt == datadt: - return False # skip bars that come again in the filter + return False # 跳过 filter 中再次出现的 bar - self.lastdt = datadt # keep ref to last seen bar + self.lastdt = datadt # 保留最近见到的 bar 引用 - # Make a copy of current data for ohlbar + # 复制当前 data,生成 ohlbar ohlbar = [data.lines[i][0] for i in range(data.size())] - closebar = ohlbar[:] # Make a copy for the close + closebar = ohlbar[:] # 为 close 生成副本 - # replace close price with o-h-l average + # 用 o-h-l 平均值替换 close price ohlprice = ohlbar[data.Open] + ohlbar[data.High] + ohlbar[data.Low] ohlbar[data.Close] = ohlprice / 3.0 - vol = ohlbar[data.Volume] # adjust volume + vol = ohlbar[data.Volume] # 调整 volume ohlbar[data.Volume] = vohl = int(vol * (1.0 - self.p.closevol)) - oi = ohlbar[data.OpenInterest] # adjust open interst + oi = ohlbar[data.OpenInterest] # 调整 open interest ohlbar[data.OpenInterest] = 0 - # Adjust times + # 调整时间 dt = datetime.datetime.combine(datadt, data.p.sessionstart) ohlbar[data.DateTime] = data.date2num(dt) - # Ajust closebar to generate a single tick -> close price + # 调整 closebar,生成单 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 + # 更新 stream + data.backwards(force=True) # 从 stream 移除已复制的 bar + data._add2stack(ohlbar) # 将 ohlbar 加入 stack + # 将第 2 部分加入 stash,延后到下一轮处理 data._add2stack(closebar, stash=True) - return False # initial tick can be further processed from stack + return False # 初始 tick 可继续从 stack 处理 diff --git a/backtrader/filters/calendardays.py b/backtrader/filters/calendardays.py index 8f1345eb2..938eb5cc5 100644 --- a/backtrader/filters/calendardays.py +++ b/backtrader/filters/calendardays.py @@ -29,24 +29,22 @@ class CalendarDays(with_metaclass(metabase.MetaParams, object)): - ''' - Bar Filler to add missing calendar days to trading days - - Params: - - - fill_price (def: None): - - > 0: The given value to fill - 0 or None: Use the last known closing price - -1: Use the midpoint of the last bar (High-Low average) - - - fill_vol (def: float('NaN')): - - Value to use to fill the missing volume - - - fill_oi (def: float('NaN')): - - Value to use to fill the missing Open Interest + '''为交易日之间缺失的自然日补 bar 的 Bar Filler。 + + Args: + fill_price: 缺失 bar 使用的价格,默认 ``None``。 + 大于 0 时使用给定值;为 0 或 ``None`` 时使用上一根已知 close; + 为 ``-1`` 时使用上一根 bar 的 midpoint(High-Low average)。 + fill_vol: 缺失 bar 使用的 volume,默认 ``NaN``。 + fill_oi: 缺失 bar 使用的 open interest,默认 ``NaN``。 + + Returns: + None: filter 会把补出的 bar 加入 data stack。 + + --- + >>> import backtrader as bt + >>> data = bt.feeds.GenericCSVData(dataname='daily.csv') + >>> data.addfilter(CalendarDays, fill_price=None) ''' params = (('fill_price', None), ('fill_vol', float('NaN')), @@ -59,33 +57,36 @@ def __init__(self, data): pass def __call__(self, data): - ''' - If the data has a gap larger than 1 day amongst bars, the missing bars - are added to the stream. + '''处理一根 data bar,并在自然日 gap 大于 1 天时补 bar。 - Params: - - data: the data source to filter/process + Args: + data: 要过滤/处理的 data source。 Returns: - - False (always): this filter does not remove bars from the stream + bool: 始终返回 ``False``,表示该 filter 不从 stream 中移除 bar。 ''' dt = data.datetime.date() - if (dt - self.lastdt) > self.ONEDAY: # gap in place + if (dt - self.lastdt) > self.ONEDAY: # 存在 gap self._fillbars(data, dt, self.lastdt) self.lastdt = dt - return False # no bar has been removed from the stream + return False # 未从 stream 中移除 bar def _fillbars(self, data, dt, lastdt): - ''' - Fills one by one bars as needed from time_start to time_end + '''按需逐根填补从 ``lastdt`` 到 ``dt`` 之间的 bar。 + + Args: + data: 要补 bar 的 data source。 + dt: 当前 bar 的日期。 + lastdt: 上一根 bar 的日期。 - Invalidates the control dtime_prev if requested + Returns: + None: 补出的 bar 会加入 data stack。 ''' - tm = data.datetime.time(0) # get time part + tm = data.datetime.time(0) # 获取 time 部分 - # Same price for all bars + # 所有补 bar 使用同一价格 if self.p.fill_price > 0: price = self.p.fill_price elif not self.p.fill_price: @@ -96,25 +97,25 @@ def _fillbars(self, data, dt, lastdt): while lastdt < dt: lastdt += self.ONEDAY - # Prepare an array of the needed size + # 准备所需大小的数组 bar = [float('Nan')] * data.size() - # Fill the datetime + # 填充 datetime bar[data.DateTime] = data.date2num(datetime.combine(lastdt, tm)) - # Fill price fields + # 填充 price 字段 for pricetype in [data.Open, data.High, data.Low, data.Close]: bar[pricetype] = price - # Fill volume and open interest + # 填充 volume 和 open interest bar[data.Volume] = self.p.fill_vol bar[data.OpenInterest] = self.p.fill_oi - # Fill extra lines the data feed may have defined beyond DateTime + # 填充 data feed 可能在 DateTime 之后定义的额外 lines for i in range(data.DateTime + 1, data.size()): bar[i] = data.lines[i][0] - # Add this constructed bar to the stack of the stream + # 将构造出的 bar 加入 stream stack data._add2stack(bar) - # Save to stack the bar that signaled the gap + # 将触发 gap 的 bar 保存到 stack data._save2stack(erase=True) diff --git a/backtrader/filters/datafiller.py b/backtrader/filters/datafiller.py index 87fe0521c..02b26d430 100644 --- a/backtrader/filters/datafiller.py +++ b/backtrader/filters/datafiller.py @@ -28,28 +28,29 @@ class DataFiller(AbstractDataBase): - '''This class will fill gaps in the source data using the following - information bits from the underlying data source + '''根据底层 data source 信息填补缺失 bar 的 data feed。 - - timeframe and compression to dimension the output bars + 填补逻辑会使用底层 data source 的 timeframe、compression、sessionstart + 和 sessionend 来确定输出 bar 的时间间隔。 - - sessionstart and sessionend + 例如分钟级 data 在 10:31 和 10:34 之间缺少 bar,本类会用上一根 bar + (10:31)的 close 价格补出 10:32 和 10:33。 - If a data feed has missing bars in between 10:31 and 10:34 and the - timeframe is minutes, the output will be filled with bars for minutes - 10:32 and 10:33 using the closing price of the last bar (10:31) + Args: + fill_price: 缺失 bar 使用的价格,默认 ``None``。如果为 ``None`` 或 + 求值为 ``False``,使用上一根 bar 的 close;否则使用传入值,例如 + ``float('NaN')``。 + fill_vol: 缺失 bar 使用的 volume,默认 ``NaN``。 + fill_oi: 缺失 bar 使用的 openinterest,默认 ``NaN``。 - Bars can be missinga amongst other things because + Returns: + bool: ``_load`` 在输出原始或补出的 bar 时返回 ``True``;底层数据耗尽 + 时返回 ``False``。 - Params: - - ``fill_price`` (def: None): if None (or evaluates to False),the - closing price will be used, else the passed value (which can be - for example 'NaN' to have a missing bar in terms of evaluation but - present in terms of time - - - ``fill_vol`` (def: NaN): used to fill the volume of missing bars - - - ``fill_oi`` (def: NaN): used to fill the openinterest of missing bars + --- + >>> import backtrader as bt + >>> data = bt.feeds.GenericCSVData(dataname='intraday.csv') + >>> filled = DataFiller(dataname=data, fill_vol=0.0) ''' params = ( @@ -65,23 +66,23 @@ def start(self): def preload(self): if len(self.p.dataname) == self.p.dataname.buflen(): - # if data is not preloaded .... do it + # 如果 data 尚未 preload,则执行 preload self.p.dataname.start() self.p.dataname.preload() self.p.dataname.home() - # Copy timeframe from data after start (some sources do autodetection) + # start 后从 data 复制 timeframe(部分 source 会自动检测) self.p.timeframe = self._timeframe = self.p.dataname._timeframe self.p.compression = self._compression = self.p.dataname._compression super(DataFiller, self).preload() def _copyfromdata(self): - # Data is allowed - Copy size which is "number of lines" + # data 被允许通过,复制 line 数量对应的数据 for i in range(self.p.dataname.size()): self.lines[i][0] = self.p.dataname.lines[i][0] - self._dbar = False # invalidate flag for read bar + self._dbar = False # 使已读取 bar 的标记失效 return True @@ -100,7 +101,7 @@ def _frombars(self): return True - # Minimum delta unit in between bars + # bar 之间的最小 delta 单位 _tdeltas = { TimeFrame.Minutes: timedelta(seconds=60), TimeFrame.Seconds: timedelta(seconds=1), @@ -109,68 +110,67 @@ def _frombars(self): def _load(self): if not len(self.p.dataname): - self.p.dataname.start() # start data if not done somewhere else + self.p.dataname.start() # 如果其他地方尚未 start data,则 start - # Copy from underlying data + # 从底层 data 复制 self._timeframe = self.p.dataname._timeframe self._compression = self.p.dataname._compression self.p.timeframe = self._timeframe self.p.compression = self._compression - # Calculate and save timedelta for timeframe + # 计算并保存 timeframe 对应的 timedelta self._tdunit = self._tdeltas[self._timeframe] self._tdunit *= self._compression if self._fillbars: return self._frombars() - # use existing bar or fetch a bar + # 使用现有 bar 或获取新 bar self._dbar = self._dbar or self.p.dataname.next() if not self._dbar: - return False # no more data + return False # 没有更多 data if len(self) == 1: - # Cannot yet look backwards - deliver data as is + # 还不能向后查看,按原样输出 data return self._copyfromdata() - # previous (delivered) close + # 上一根已输出 bar 的 close pclose = self.lines.close[-1] - # Get time of previous (already delivered) bar + # 获取上一根已输出 bar 的时间 dtime_prev = self.lines.datetime.datetime(-1) - # Get time of current (from data source) bar + # 获取当前底层 data source bar 的时间 dtime_cur = self.p.dataname.datetime.datetime(0) - # Calculate session end for previous bar + # 计算上一根 bar 所在 session 的结束时间 send = datetime.combine(dtime_prev.date(), self.p.dataname.sessionend) - if dtime_cur > send: # if jumped boundary - # 1. check for missing bars until boundary (end) + if dtime_cur > send: # 如果跨过 session 边界 + # 1. 检查到 session 结束前是否缺 bar dtime_prev += self._tdunit while dtime_prev < send: self._fillbars.append((dtime_prev, pclose)) dtime_prev += self._tdunit - # Calculate session start for new bar + # 计算新 bar 所在 session 的开始时间 sstart = datetime.combine( dtime_cur.date(), self.p.dataname.sessionstart) - # 2. check for missing bars from new boundary (start) - # check gap from new sessionstart + # 2. 检查从新 session 起点到当前 bar 前是否缺 bar while sstart < dtime_cur: self._fillbars.append((sstart, pclose)) sstart += self._tdunit else: - # no boundary jumped - check gap until current time + # 未跨边界,检查到当前时间前的 gap dtime_prev += self._tdunit while dtime_prev < dtime_cur: self._fillbars.append((dtime_prev, pclose)) dtime_prev += self._tdunit if self._fillbars: - self._dbar = True # flag a pending data bar is available + self._dbar = True # 标记有一个 pending data bar 可用 - # return an accumulated bar in current cycle + # 在当前 cycle 返回一根累积出来的 bar return self._frombars() return self._copyfromdata() diff --git a/backtrader/filters/datafilter.py b/backtrader/filters/datafilter.py index f9fa8e25f..8c999060f 100644 --- a/backtrader/filters/datafilter.py +++ b/backtrader/filters/datafilter.py @@ -25,30 +25,36 @@ class DataFilter(bt.AbstractDataBase): - ''' - This class filters out bars from a given data source. In addition to the - standard parameters of a DataBase it takes a ``funcfilter`` parameter which - can be any callable + '''包装 data source,并按 callable 条件过滤 bar 的 data feed。 - Logic: + 除 ``DataBase`` 标准参数外,该类接收 ``funcfilter`` 参数。``funcfilter`` + 会以底层 data source 为参数被调用。 - - ``funcfilter`` will be called with the underlying data source + Args: + funcfilter: 任意 callable。返回 ``True`` 时保留当前 data source bar; + 返回 ``False`` 时丢弃当前 bar。 - It can be any callable + Returns: + bool: ``_load`` 在成功加载一个通过过滤的 bar 时返回 ``True``;底层 + data source 耗尽时返回 ``False``。 - - Return value ``True``: current data source bar values will used - - Return value ``False``: current data source bar values will discarded + --- + >>> import backtrader as bt + >>> def keep_positive_close(data): + ... return data.close[0] > 0 + >>> data = bt.feeds.GenericCSVData(dataname='prices.csv') + >>> filtered = DataFilter(dataname=data, funcfilter=keep_positive_close) ''' params = (('funcfilter', None),) def preload(self): if len(self.p.dataname) == self.p.dataname.buflen(): - # if data is not preloaded .... do it + # 如果 data 尚未 preload,则执行 preload self.p.dataname.start() self.p.dataname.preload() self.p.dataname.home() - # Copy timeframe from data after start (some sources do autodetection) + # start 后从 data 复制 timeframe(部分 source 会自动检测) self.p.timeframe = self._timeframe = self.p.dataname._timeframe self.p.compression = self._compression = self.p.dataname._compression @@ -56,18 +62,18 @@ def preload(self): def _load(self): if not len(self.p.dataname): - self.p.dataname.start() # start data if not done somewhere else + self.p.dataname.start() # 如果其他地方尚未 start data,则 start - # Tell underlying source to get next data + # 通知底层 source 获取下一条 data while self.p.dataname.next(): - # Try to load the data from the underlying source + # 尝试从底层 source 加载 data if not self.p.funcfilter(self.p.dataname): continue - # Data is allowed - Copy size which is "number of lines" + # data 被允许通过,复制 line 数量对应的数据 for i in range(self.p.dataname.size()): self.lines[i][0] = self.p.dataname.lines[i][0] return True - return False # no more data from underlying source + return False # 底层 source 没有更多 data diff --git a/backtrader/filters/daysteps.py b/backtrader/filters/daysteps.py index 7d56bd222..a072528de 100644 --- a/backtrader/filters/daysteps.py +++ b/backtrader/filters/daysteps.py @@ -23,18 +23,28 @@ class BarReplayer_Open(object): - ''' - This filters splits a bar in two parts: + '''将一根 bar 拆成 open bar 和完整 OHLC bar 的 filter。 + + 拆分结果: + + - ``Open``: 使用原 bar 的 opening price 生成初始 price bar,四个 OHLC + 组件相等。该初始 bar 的 volume/openinterest 为 0。 + + - ``OHLC``: 输出完整原始 bar,并保留原始 ``volume``/``openinterest``。 - - ``Open``: the opening price of the bar will be used to deliver an - initial price bar in which the four components (OHLC) are equal + 该拆分可模拟 replay,而无需使用 *replay* filter。 - The volume/openinterest fields are 0 for this initial bar + Args: + 无。 - - ``OHLC``: the original bar is delivered complete with the original - ``volume``/``openinterest`` + Returns: + bool: stream 长度未变化时返回 ``True``;输出 pending bar 时返回 + ``False``。 - The split simulates a replay without the need to use the *replay* filter. + --- + >>> import backtrader as bt + >>> data = bt.feeds.GenericCSVData(dataname='daily.csv') + >>> data.addfilter(BarReplayer_Open) ''' def __init__(self, data): self.pendingbar = None @@ -44,40 +54,48 @@ def __init__(self, data): def __call__(self, data): ret = True - # Make a copy of the new bar and remove it from stream + # 复制新 bar,并从 stream 中移除 newbar = [data.lines[i][0] for i in range(data.size())] - data.backwards() # remove the copied bar from stream + data.backwards() # 从 stream 中移除已复制的 bar - openbar = newbar[:] # Make an open only bar + openbar = newbar[:] # 生成只有 open 的 bar o = newbar[data.Open] for field_idx in [data.High, data.Low, data.Close]: openbar[field_idx] = o - # Nullify Volume/OpenInteres at the open + # 将 open 阶段的 Volume/OpenInterest 置零 openbar[data.Volume] = 0.0 openbar[data.OpenInterest] = 0.0 - # Overwrite the new data bar with our pending data - except start point + # 用 pending data 覆盖新的 data bar,起点除外 if self.pendingbar is not None: data._updatebar(self.pendingbar) ret = False - self.pendingbar = newbar # update the pending bar to the new bar - data._add2stack(openbar) # Add the openbar to the stack for processing + self.pendingbar = newbar # 将 pending bar 更新为新 bar + data._add2stack(openbar) # 将 openbar 加入 stack 等待处理 - return ret # the length of the stream was not changed + return ret # stream 长度未变化 def last(self, data): - '''Called when the data is no longer producing bars - Can be called multiple times. It has the chance to (for example) - produce extra bars''' + '''当 data 不再产生 bar 时调用。 + + 该方法可以被多次调用,可用于输出额外 bar。 + + Args: + data: 要处理的 data source。 + + Returns: + bool: 输出 pending bar 时返回 ``True``;没有可输出内容时返回 + ``False``。 + ''' if self.pendingbar is not None: - data.backwards() # remove delivered open bar - data._add2stack(self.pendingbar) # add remaining - self.pendingbar = None # No further action - return True # something delivered + data.backwards() # 移除已交付的 open bar + data._add2stack(self.pendingbar) # 加入剩余 bar + self.pendingbar = None # 无需进一步动作 + return True # 已交付内容 - return False # nothing delivered here + return False # 此处没有交付内容 # Alias diff --git a/backtrader/filters/heikinashi.py b/backtrader/filters/heikinashi.py index 08165e73a..0fd1f906d 100644 --- a/backtrader/filters/heikinashi.py +++ b/backtrader/filters/heikinashi.py @@ -26,14 +26,22 @@ class HeikinAshi(object): - ''' - The filter remodels the open, high, low, close to make HeikinAshi - candlesticks + '''将 open、high、low、close 重塑为 Heikin Ashi K 线的 filter。 + + Args: + 无。 + + Returns: + bool: 始终返回 ``False``,表示 data stream 长度不变。 See: - https://en.wikipedia.org/wiki/Candlestick_chart#Heikin_Ashi_candlesticks - http://stockcharts.com/school/doku.php?id=chart_school:chart_analysis:heikin_ashi + --- + >>> import backtrader as bt + >>> data = bt.feeds.GenericCSVData(dataname='daily.csv') + >>> data.addfilter(HeikinAshi) ''' def __init__(self, data): pass @@ -48,7 +56,7 @@ def __call__(self, data): data.high[0] = max(ha_open0, ha_close0, h) data.low[0] = min(ha_open0, ha_close0, l) - else: # len is 1, no lookback is possible + else: # len 为 1,无法 lookback data.open[0] = ha_open0 = (o + c) / 2.0 - return False # length of data stream is unaltered + return False # data stream 长度不变 diff --git a/backtrader/filters/renko.py b/backtrader/filters/renko.py index d07c080e7..ebe4173a9 100644 --- a/backtrader/filters/renko.py +++ b/backtrader/filters/renko.py @@ -29,38 +29,39 @@ class Renko(Filter): - '''Modify the data stream to draw Renko bars (or bricks) - - Params: - - - ``hilo`` (default: *False*) Use high and low instead of close to decide - if a new brick is needed - - - ``size`` (default: *None*) The size to consider for each brick - - - ``autosize`` (default: *20.0*) If *size* is *None*, this will be used - to autocalculate the size of the bricks (simply dividing the current - price by the given value) - - - ``dynamic`` (default: *False*) If *True* and using *autosize*, the size - of the bricks will be recalculated when moving to a new brick. This - will of course eliminate the perfect alignment of Renko bricks. - - - ``align`` (default: *1.0*) Factor use to align the price boundaries of - the bricks. If the price is for example *3563.25* and *align* is - *10.0*, the resulting aligned price will be *3560*. The calculation: + '''修改 data stream,用于绘制 Renko bars(bricks)的 filter。 + + Args: + hilo (bool): 是否使用 high/low 而不是 close 来判断是否需要新 brick, + 默认 ``False``。 + size: 每个 brick 使用的 size,默认 ``None``。 + autosize (float): ``size`` 为 ``None`` 时用于自动计算 brick size 的值, + 默认 ``20.0``。计算方式是用当前价格除以该值。 + dynamic (bool): 在使用 ``autosize`` 时,是否在移动到新 brick 时重新 + 计算 brick size,默认 ``False``。启用后会破坏 Renko bricks 的 + 完美对齐。 + align (float): 用于对齐 brick price 边界的 factor,默认 ``1.0``。 + 例如 price 为 ``3563.25``、align 为 ``10.0`` 时,对齐结果为 + ``3560``: - 3563.25 / 10.0 = 356.325 - - round it and remove the decimals -> 356 + - round 并移除 decimals -> 356 - 356 * 10.0 -> 3560 - - ``roundstart`` (default: *True*) If *True*, round the initial start - value to int. Else keep the original value, which should aid when - backtesting penny stocks + roundstart (bool): 是否将初始 start value round 为 int,默认 + ``True``。设为 ``False`` 可在回测 penny stocks 时保留原始值。 + + Returns: + bool: 输出 Renko brick 时返回 ``False``;当前 bar 未形成新 brick 时 + 回退 data 并返回 ``True``,表示 stream 长度改变,需要获取新 bar。 See: - http://stockcharts.com/school/doku.php?id=chart_school:chart_analysis:renko + --- + >>> import backtrader as bt + >>> data = bt.feeds.GenericCSVData(dataname='daily.csv') + >>> data.addfilter(Renko, size=2.0, align=1.0) ''' params = ( @@ -74,7 +75,7 @@ class Renko(Filter): def nextstart(self, data): o = data.open[0] - o = round(o / self.p.align, 0) * self.p.align # aligned + o = round(o / self.p.align, 0) * self.p.align # 已对齐 self._size = self.p.size or float(o // self.p.autosize) if self.p.roundstart: o = int(o) @@ -94,13 +95,13 @@ def next(self, data): hiprice = loprice = c if hiprice >= self._top: - # deliver a renko brick from top -> top + size + # 输出一个从 top -> top + size 的 renko brick self._bot = bot = self._top if self.p.size is None and self.p.dynamic: self._size = float(c // self.p.autosize) top = bot + self._size - top = round(top / self.p.align, 0) * self.p.align # aligned + top = round(top / self.p.align, 0) * self.p.align # 已对齐 else: top = bot + self._size @@ -112,16 +113,16 @@ def next(self, data): data.close[0] = top data.volume[0] = 0.0 data.openinterest[0] = 0.0 - return False # length of data stream is unaltered + return False # data stream 长度不变 elif loprice <= self._bot: - # deliver a renko brick from bot -> bot - size + # 输出一个从 bot -> bot - size 的 renko brick self._top = top = self._bot if self.p.size is None and self.p.dynamic: self._size = float(c // self.p.autosize) bot = top - self._size - bot = round(bot / self.p.align, 0) * self.p.align # aligned + bot = round(bot / self.p.align, 0) * self.p.align # 已对齐 else: bot = top - self._size @@ -133,7 +134,7 @@ def next(self, data): data.close[0] = bot data.volume[0] = 0.0 data.openinterest[0] = 0.0 - return False # length of data stream is unaltered + return False # data stream 长度不变 data.backwards() - return True # length of stream was changed, get new bar + return True # stream 长度已改变,获取新 bar diff --git a/backtrader/filters/session.py b/backtrader/filters/session.py index 60e437b5b..739fa1755 100644 --- a/backtrader/filters/session.py +++ b/backtrader/filters/session.py @@ -29,32 +29,28 @@ class SessionFiller(with_metaclass(metabase.MetaParams, object)): - ''' - Bar Filler for a Data Source inside the declared session start/end times. - - The fill bars are constructed using the declared Data Source ``timeframe`` - and ``compression`` (used to calculate the intervening missing times) - - Params: - - - fill_price (def: None): - - If None is passed, the closing price of the previous bar will be - used. To end up with a bar which for example takes time but it is not - displayed in a plot ... use float('Nan') - - - fill_vol (def: float('NaN')): - - Value to use to fill the missing volume - - - fill_oi (def: float('NaN')): - - Value to use to fill the missing Open Interest - - - skip_first_fill (def: True): - - Upon seeing the 1st valid bar do not fill from the sessionstart up to - that bar + '''在声明的 session start/end 时间内为 data source 补 bar 的 Bar Filler。 + + 补出的 bar 会使用 data source 声明的 ``timeframe`` 和 ``compression`` + 来计算缺失时间点。 + + Args: + fill_price: 缺失 bar 使用的价格,默认 ``None``。如果为 ``None``, + 使用上一根 bar 的 close。也可以传入 ``float('NaN')``,让该 bar + 占用时间但不在图上显示出有效价格。 + fill_vol: 缺失 bar 使用的 volume,默认 ``NaN``。 + fill_oi: 缺失 bar 使用的 open interest,默认 ``NaN``。 + skip_first_fill (bool): 看到第 1 根有效 bar 时,是否跳过从 + sessionstart 到该 bar 的填补,默认 ``True``。 + + Returns: + None: filter 会把补出的 bar 加入 data stack。 + + --- + >>> import backtrader as bt + >>> data = bt.feeds.GenericCSVData(dataname='intraday.csv', + ... timeframe=bt.TimeFrame.Minutes) + >>> data.addfilter(SessionFiller, fill_vol=0.0) ''' params = (('fill_price', None), ('fill_vol', float('NaN')), @@ -63,7 +59,7 @@ class SessionFiller(with_metaclass(metabase.MetaParams, object)): MAXDATE = datetime.max - # Minimum delta unit in between bars + # bar 之间的最小 delta 单位 _tdeltas = { TimeFrame.Minutes: timedelta(seconds=60), TimeFrame.Seconds: timedelta(seconds=1), @@ -71,60 +67,56 @@ class SessionFiller(with_metaclass(metabase.MetaParams, object)): } def __init__(self, data): - # Calculate and save timedelta for timeframe + # 计算并保存 timeframe 对应的 timedelta self._tdframe = self._tdeltas[data._timeframe] self._tdunit = self._tdeltas[data._timeframe] * data._compression - self.seenbar = False # control if at least one bar has been seen - self.sessend = self.MAXDATE # maxdate is the control for session bar + self.seenbar = False # 控制是否至少见过一根 bar + self.sessend = self.MAXDATE # maxdate 是 session bar 的控制标记 def __call__(self, data): - ''' - Params: - - data: the data source to filter/process + '''处理一根 data bar,并按 session 边界补出缺失 bar。 - Returns: - - False (always) because this filter does not remove bars from the - stream - - The logic (starting with a session end control flag of MAXDATE) + Args: + data: 要过滤/处理的 data source。 - - If new bar is over session end (never true for 1st bar) - - Fill up to session end. Reset sessionend to MAXDATE & fall through + Returns: + bool: 添加了补 bar 或需要重新处理 stream 时返回 ``True``;否则返回 + ``False``。 - - If session end is flagged as MAXDATE + 逻辑从 ``MAXDATE`` 作为 session end 控制标记开始: - Recalculate session limits and check whether the bar is within them + - 如果新 bar 超过 session end(第 1 根 bar 不会出现这种情况), + 补到 session end,并将 session end 重置为 ``MAXDATE`` 后继续。 - if so, fill up and record the last seen tim + - 如果 session end 标记为 ``MAXDATE``,重新计算 session 边界, + 检查 bar 是否在边界内;如果在,补齐并记录最后看到的时间。 - - Else ... the incoming bar is in the session, fill up to it + - 否则 incoming bar 位于 session 内,补到该 bar 为止。 ''' - # Get time of current (from data source) bar + # 获取当前底层 data source bar 的时间 ret = False dtime_cur = data.datetime.datetime() if dtime_cur > self.sessend: - # bar over session end - fill up and invalidate - # Do not put current bar in stack to let it be evaluated below - # Fill up to endsession + smallest unit of timeframe + # bar 超过 session end,补齐并使控制标记失效 + # 不把当前 bar 放入 stack,让它在下方继续被评估 + # 补到 session end + timeframe 最小单位 ret = self._fillbars(data, self.dtime_prev, self.sessend + self._tdframe, tostack=False) self.sessend = self.MAXDATE - # Fall through from previous check ... the bar which is over the - # session could already be in a new session and within the limits + # 从前一检查继续:超过旧 session 的 bar 可能已经处于新 session 内 if self.sessend == self.MAXDATE: - # No bar seen yet or one went over previous session limit + # 尚未见过 bar,或某根 bar 超过了上一 session 边界 ddate = dtime_cur.date() sessstart = datetime.combine(ddate, data.p.sessionstart) self.sessend = sessend = datetime.combine(ddate, data.p.sessionend) if sessstart <= dtime_cur <= sessend: - # 1st bar from session in the session - fill from session start + # session 内的第 1 根 bar:从 session start 开始填补 if self.seenbar or not self.p.skip_first_fill: ret = self._fillbars(data, sessstart - self._tdunit, dtime_cur) @@ -133,19 +125,25 @@ def __call__(self, data): self.dtime_prev = dtime_cur else: - # Seen a previous bar and this is in the session - fill up to it + # 已见过前一根 bar,且当前 bar 在 session 内:补到当前 bar ret = self._fillbars(data, self.dtime_prev, dtime_cur) self.dtime_prev = dtime_cur return ret def _fillbars(self, data, time_start, time_end, tostack=True): - ''' - Fills one by one bars as needed from time_start to time_end + '''按需逐根填补从 ``time_start`` 到 ``time_end`` 之间的 bar。 + + Args: + data: 要补 bar 的 data source。 + time_start: 填补起始时间。 + time_end: 填补结束时间。 + tostack (bool): 是否把触发填补的 bar 保存回 stack。 - Invalidates the control dtime_prev if requested + Returns: + bool: 有补 bar 或 ``tostack`` 为 ``False`` 时返回 ``True``。 ''' - # Control flag - bars added to the stack + # 控制标记:是否有 bar 加入 stack dirty = 0 time_start += self._tdunit @@ -159,86 +157,110 @@ def _fillbars(self, data, time_start, time_end, tostack=True): return bool(dirty) or not tostack def _fillbar(self, data, dtime): - # Prepare an array of the needed size + # 准备所需大小的数组 bar = [float('Nan')] * data.size() - # Fill datetime + # 填充 datetime bar[data.DateTime] = data.date2num(dtime) - # Fill the prices + # 填充 price price = self.p.fill_price or data.close[-1] for pricetype in [data.Open, data.High, data.Low, data.Close]: bar[pricetype] = price - # Fill volume and open interest + # 填充 volume 和 open interest bar[data.Volume] = self.p.fill_vol bar[data.OpenInterest] = self.p.fill_oi - # Fill extra lines the data feed may have defined beyond DateTime + # 填充 data feed 可能在 DateTime 之后定义的额外 lines for i in range(data.DateTime + 1, data.size()): bar[i] = data.lines[i][0] - # Add tot he stack of bars to save + # 加入待保存 bar stack data._add2stack(bar) return True class SessionFilterSimple(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) + '''过滤常规 session 时间之外日内 bar 的 simple filter。 - This is a "simple" filter and must NOT manage the stack of the data (passed - during init and __call__) + 该 filter 可应用到 data source,用于过滤 pre/post market data 等常规 + session 之外的日内 bar。 - It needs no "last" method because it has nothing to deliver + 这是 "simple" filter,不管理传入 ``__init__`` 和 ``__call__`` 的 data + stack。它没有需要额外交付的数据,因此不需要 ``last`` 方法。Bar + management 会由 ``DataBase.addfilter_simple`` 添加的 + ``SimpleFilterWrapper`` 完成。 - Bar Management will be done by the SimpleFilterWrapper class made which is - added durint the DataBase.addfilter_simple call + Args: + 无。 + + Returns: + bool: 当前 bar 在 session 内返回 ``False``;在 session 外返回 + ``True``,表示过滤当前 bar。 + + --- + >>> import backtrader as bt + >>> data = bt.feeds.GenericCSVData(dataname='intraday.csv') + >>> data.addfilter_simple(SessionFilterSimple) ''' def __init__(self, data): pass def __call__(self, data): - ''' - Return Values: + '''判断当前 bar 是否应被过滤。 + + Args: + data: 要过滤/处理的 data source。 - - False: nothing to filter - - True: filter current bar (because it's not in the session times) + Returns: + bool: ``False`` 表示无需过滤;``True`` 表示当前 bar 不在 session + 时间内,应被过滤。 ''' - # Both ends of the comparison are in the session + # 比较的两端都位于 session 中 return not ( data.p.sessionstart <= data.datetime.time(0) <= data.p.sessionend) 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) + '''过滤常规 session 时间之外日内 bar 的非 simple filter。 + + 该 filter 可应用到 data source,用于过滤 pre/post market data 等常规 + session 之外的日内 bar。 + + 这是 "non-simple" filter,必须管理传入 ``__init__`` 和 ``__call__`` 的 + data stack。它没有需要额外交付的数据,因此不需要 ``last`` 方法。 - This is a "non-simple" filter and must manage the stack of the data (passed - during init and __call__) + Args: + 无。 - It needs no "last" method because it has nothing to deliver + Returns: + bool: 当前 bar 在 session 内返回 ``False``;在 session 外移除 bar 并 + 返回 ``True``。 + + --- + >>> import backtrader as bt + >>> data = bt.feeds.GenericCSVData(dataname='intraday.csv') + >>> data.addfilter(SessionFilter) ''' def __init__(self, data): pass def __call__(self, data): - ''' - Return Values: + '''判断并处理当前 bar 是否应被过滤。 - - False: data stream was not touched - - True: data stream was manipulated (bar outside of session times and - - removed) + Args: + data: 要过滤/处理的 data source。 + + Returns: + bool: ``False`` 表示 data stream 未改动;``True`` 表示 data stream + 已被改动,即 session 时间外的 bar 已被移除。 ''' if data.p.sessionstart <= data.datetime.time(0) <= data.p.sessionend: - # Both ends of the comparison are in the session - return False # say the stream is untouched + # 比较的两端都位于 session 中 + return False # 表示 stream 未改动 - # bar outside of the regular session times - data.backwards() # remove bar from data stack - return True # signal the data was manipulated + # bar 位于常规 session 时间之外 + data.backwards() # 从 data stack 移除 bar + return True # 表示 data 已被改动 diff --git a/backtrader/flt.py b/backtrader/flt.py index 018385dae..ecb7b31aa 100644 --- a/backtrader/flt.py +++ b/backtrader/flt.py @@ -30,17 +30,25 @@ class MetaFilter(MetaParams): + '''Filter metaclass 的基类,用于承载参数元信息。''' pass class Filter(with_metaclass(MetaParams, object)): + '''data filter 的基类,用于在 data 推进时执行过滤逻辑。''' _firsttime = True def __init__(self, data): + '''初始化 filter。 + + Args: + data: filter 绑定的数据源。 + ''' pass def __call__(self, data): + '''执行一次 filter 调用。''' if self._firsttime: self.nextstart(data) self._firsttime = False @@ -48,7 +56,9 @@ def __call__(self, data): self.next(data) def nextstart(self, data): + '''首次处理 data 时调用的 hook。''' pass def next(self, data): + '''每次处理 data 时调用的 hook。''' pass diff --git a/backtrader/functions.py b/backtrader/functions.py index 40109bba7..bbc68c40b 100644 --- a/backtrader/functions.py +++ b/backtrader/functions.py @@ -28,27 +28,32 @@ from .utils.py3 import cmp, range -# Generate a List equivalent which uses "is" for contains +# 生成一个在 contains 判断中使用 hash 等价性的 List class List(list): + '''List 的轻量变体,用于按对象 hash 判断包含关系。''' + def __contains__(self, other): return any(x.__hash__() == other.__hash__() for x in self) class Logic(LineActions): + '''line operation 的基类,用于把参数转换为可按 bar 访问的 array。''' + def __init__(self, *args): super(Logic, self).__init__() self.args = [self.arrayize(arg) for arg in args] class DivByZero(Logic): - '''This operation is a Lines object and fills it values by executing a - division on the numerator / denominator arguments and avoiding a division - by zero exception by checking the denominator + '''除法 line operation,遇到分母为 0 时返回指定 fallback。 + + Args: + a: 分子,numeric 或 iterable 对象,通常是 Lines object。 + b: 分母,numeric 或 iterable 对象,通常是 Lines object。 + zero: 分母为 0 时写入的值,默认 ``0.0``。 - Params: - - a: numerator (numeric or iterable object ... mostly a Lines object) - - b: denominator (numeric or iterable object ... mostly a Lines object) - - zero (def: 0.0): value to apply if division by zero would be raised + Returns: + DivByZero: 输出 ``a / b`` 或 fallback 的 Lines object。 ''' def __init__(self, a, b, zero=0.0): @@ -62,7 +67,7 @@ def next(self): self[0] = self.a[0] / b if b else self.zero def once(self, start, end): - # cache python dictionary lookups + # 缓存 Python dictionary lookup dst = self.array srca = self.a.array srcb = self.b.array @@ -74,16 +79,16 @@ def once(self, start, end): class DivZeroByZero(Logic): - '''This operation is a Lines object and fills it values by executing a - division on the numerator / denominator arguments and avoiding a division - by zero exception or an indetermination by checking the - denominator/numerator pair - - Params: - - a: numerator (numeric or iterable object ... mostly a Lines object) - - b: denominator (numeric or iterable object ... mostly a Lines object) - - single (def: +inf): value to apply if division is x / 0 - - dual (def: 0.0): value to apply if division is 0 / 0 + '''除法 line operation,区分 ``x / 0`` 与 ``0 / 0`` 两种 fallback。 + + Args: + a: 分子,numeric 或 iterable 对象,通常是 Lines object。 + b: 分母,numeric 或 iterable 对象,通常是 Lines object。 + single: ``x / 0`` 时写入的值,默认 ``+inf``。 + dual: ``0 / 0`` 时写入的值,默认 ``0.0``。 + + Returns: + DivZeroByZero: 输出除法结果或对应 fallback 的 Lines object。 ''' def __init__(self, a, b, single=float('inf'), dual=0.0): super(DivZeroByZero, self).__init__(a, b) @@ -101,7 +106,7 @@ def next(self): self[0] = self.a[0] / b def once(self, start, end): - # cache python dictionary lookups + # 缓存 Python dictionary lookup dst = self.array srca = self.a.array srcb = self.b.array @@ -118,6 +123,8 @@ def once(self, start, end): class Cmp(Logic): + '''比较两个输入并输出 ``cmp(a, b)`` 的 line operation。''' + def __init__(self, a, b): super(Cmp, self).__init__(a, b) self.a = self.args[0] @@ -127,7 +134,7 @@ def next(self): self[0] = cmp(self.a[0], self.b[0]) def once(self, start, end): - # cache python dictionary lookups + # 缓存 Python dictionary lookup dst = self.array srca = self.a.array srcb = self.b.array @@ -137,6 +144,8 @@ def once(self, start, end): class CmpEx(Logic): + '''扩展比较 operation,根据 ``a`` 与 ``b`` 的关系输出三个备选结果之一。''' + def __init__(self, a, b, r1, r2, r3): super(CmpEx, self).__init__(a, b, r1, r2, r3) self.a = self.args[0] @@ -149,7 +158,7 @@ def next(self): self[0] = cmp(self.a[0], self.b[0]) def once(self, start, end): - # cache python dictionary lookups + # 缓存 Python dictionary lookup dst = self.array srca = self.a.array srcb = self.b.array diff --git a/backtrader/indicator.py b/backtrader/indicator.py index b46b31785..406e1b981 100644 --- a/backtrader/indicator.py +++ b/backtrader/indicator.py @@ -44,15 +44,14 @@ def cleancache(cls): def usecache(cls, onoff): cls._icacheuse = onoff - # Object cache deactivated on 2016-08-17. If the object is being used - # inside another object, the minperiod information carried over - # influences the first usage when being modified during the 2nd usage + # object cache 于 2016-08-17 停用。如果对象被另一个对象内部使用, + # 传递过来的 minperiod 信息会在第 2 次使用时被修改,并影响第 1 次使用 def __call__(cls, *args, **kwargs): if not cls._icacheuse: return super(MetaIndicator, cls).__call__(*args, **kwargs) - # implement a cache to avoid duplicating lines actions + # 实现 cache,避免重复创建 lines actions ckey = (cls, tuple(args), tuple(kwargs.items())) # tuples hashable try: return cls._icache[ckey] @@ -65,10 +64,8 @@ def __call__(cls, *args, **kwargs): return cls._icache.setdefault(ckey, _obj) def __init__(cls, name, bases, dct): - ''' - Class has already been created ... register subclasses - ''' - # Initialize the class + '''类已经创建完成,注册其 subclasses。''' + # 初始化 class super(MetaIndicator, cls).__init__(name, bases, dct) if not cls.aliased and \ @@ -76,12 +73,12 @@ def __init__(cls, name, bases, dct): refattr = getattr(cls, cls._refname) refattr[name] = cls - # Check if next and once have both been overridden + # 检查 next 和 once 是否都被覆盖 next_over = cls.next != IndicatorBase.next once_over = cls.once != IndicatorBase.once if next_over and not once_over: - # No -> need pointer movement to once simulation via next + # 未覆盖 once 时,需要移动指针并通过 next 模拟 once cls.once = cls.once_via_next cls.preonce = cls.preonce_via_prenext cls.oncestart = cls.oncestart_via_nextstart @@ -93,13 +90,12 @@ class Indicator(with_metaclass(MetaIndicator, IndicatorBase)): csv = False def advance(self, size=1): - # Need intercepting this call to support datas with - # different lengths (timeframes) + # 需要拦截该调用,以支持不同长度/timeframe 的 datas if len(self) < len(self._clock): self.lines.advance(size=size) def preonce_via_prenext(self, start, end): - # generic implementation if prenext is overridden but preonce is not + # 当覆盖了 prenext 但未覆盖 preonce 时使用的通用实现 for i in range(start, end): for data in self.datas: data.advance() @@ -111,8 +107,7 @@ def preonce_via_prenext(self, start, end): self.prenext() def oncestart_via_nextstart(self, start, end): - # nextstart has been overriden, but oncestart has not and the code is - # here. call the overriden nextstart + # nextstart 已被覆盖但 oncestart 未被覆盖时,调用覆盖后的 nextstart for i in range(start, end): for data in self.datas: data.advance() @@ -124,7 +119,7 @@ def oncestart_via_nextstart(self, start, end): self.nextstart() def once_via_next(self, start, end): - # Not overridden, next must be there ... + # once 未被覆盖时,必须通过 next 执行 for i in range(start, end): for data in self.datas: data.advance() @@ -149,14 +144,14 @@ def donew(cls, *args, **kwargs): newplotlines.setdefault(lname, dict()) cls.plotlines = plotlines._derive(name, newplotlines, [], recurse=True) - # Create the object and set the params in place + # 创建对象并设置 params _obj, args, kwargs = \ super(MtLinePlotterIndicator, cls).donew(*args, **kwargs) _obj.owner = _obj.data.owner._clock _obj.data.lines[0].addbinding(_obj.lines[0]) - # Return the object and arguments to the chain + # 将对象和参数返回给调用链 return _obj, args, kwargs diff --git a/backtrader/indicators/__init__.py b/backtrader/indicators/__init__.py index da7735c23..f2a60737f 100644 --- a/backtrader/indicators/__init__.py +++ b/backtrader/indicators/__init__.py @@ -24,15 +24,14 @@ from backtrader import Indicator from backtrader.functions import * -# The modules below should/must define __all__ with the Indicator objects -# of prepend an "_" (underscore) to private classes/variables +# 下列模块应在 __all__ 中定义 Indicator 对象,私有类/变量则使用 "_" 前缀 from .basicops import * -# base for moving averages +# moving average 基础 from .mabase import * -# moving averages (so envelope and oscillators can be auto-generated) +# moving average(用于自动生成 envelope 和 oscillator) from .sma import * from .ema import * from .smma import * @@ -44,10 +43,10 @@ from .zlind import * from .dma import * -# depends on moving averages +# 依赖 moving average from .deviation import * -# depend on basicops, moving averages and deviations +# 依赖 basicops、moving average 和 deviation from .atr import * from .aroon import * from .bollinger import * @@ -80,7 +79,7 @@ from .dv2 import * # depends on percentrank -# Depends on Momentum +# 依赖 Momentum from .kst import * from .ichimoku import * diff --git a/backtrader/indicators/accdecoscillator.py b/backtrader/indicators/accdecoscillator.py index e3496932d..27c858032 100644 --- a/backtrader/indicators/accdecoscillator.py +++ b/backtrader/indicators/accdecoscillator.py @@ -30,10 +30,17 @@ class AccelerationDecelerationOscillator(bt.Indicator): ''' - Acceleration/Deceleration Technical Indicator (AC) measures acceleration - and deceleration of the current driving force. This indicator will change - direction before any changes in the driving force, which, it its turn, will - change its direction before the price. + Acceleration/Deceleration Technical Indicator(AC)用于衡量当前驱动力的加速和 + 减速。 + + 该指标会先于驱动力变化而改变方向,而驱动力又通常先于价格改变方向。 + + Args: + period: 对 AwesomeOscillator 做 SMA 的周期。 + movav: 使用的 Moving Average 类型。 + + Returns: + AccelerationDecelerationOscillator: 输出 ``accde`` line 的 indicator。 Formula: - AcdDecOsc = AwesomeOscillator - SMA(AwesomeOscillator, period) @@ -42,6 +49,12 @@ class AccelerationDecelerationOscillator(bt.Indicator): - https://www.metatrader5.com/en/terminal/help/indicators/bw_indicators/ao - https://www.ifcmarkets.com/en/ntx-indicators/ntx-indicators-accelerator-decelerator-oscillator + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(AccelerationDecelerationOscillator) ''' alias = ('AccDeOsc',) lines = ('accde', ) diff --git a/backtrader/indicators/aroon.py b/backtrader/indicators/aroon.py index 98782d0f6..141f836d0 100644 --- a/backtrader/indicators/aroon.py +++ b/backtrader/indicators/aroon.py @@ -26,15 +26,11 @@ class _AroonBase(Indicator): ''' - Base class which does the calculation of the AroonUp/AroonDown values and - defines the common parameters. + Aroon 的基类,用于计算 AroonUp/AroonDown 值并定义公共参数。 - It uses the class attributes _up and _down (boolean flags) to decide which - value has to be calculated. + 它使用类属性 ``_up`` 与 ``_down``(布尔标志)决定要计算哪个值。 - Values are not assigned to lines but rather stored in the "up" and "down" - instance variables, which can be used by subclasses to for assignment or - further calculations + 计算值不会直接赋给 line,而是存入实例变量 ``up`` 与 ``down``,供子类赋值或继续计算。 ''' _up = False _down = False @@ -50,9 +46,8 @@ def _plotinit(self): self.plotinfo.plotyhlines += [self.p.lowerband, self.p.upperband] def __init__(self): - # Look backwards period + 1 for current data because the formula mus - # produce values between 0 and 100 and can only do that if the - # calculated hhidx/llidx go from 0 to period (hence period + 1 values) + # 当前 data 向后看 period + 1。公式需要产出 0 到 100 之间的值, + # 只有 hhidx/llidx 能覆盖 0 到 period 时才成立,因此需要 period + 1 个值。 idxperiod = self.p.period + 1 if self._up: @@ -68,23 +63,34 @@ def __init__(self): class AroonUp(_AroonBase): ''' - This is the AroonUp from the indicator AroonUpDown developed by Tushar - Chande in 1995. + Tushar Chande 于 1995 年开发的 AroonUpDown indicator 中的 AroonUp。 + + Args: + period: 回看周期。 + upperband: 绘图时的上轨参考线。 + lowerband: 绘图时的下轨参考线。 + + Returns: + AroonUp: 输出 ``aroonup`` line 的 indicator。 Formula: - up = 100 * (period - distance to highest high) / period Note: - The lines oscillate between 0 and 100. That means that the "distance" to - the last highest or lowest must go from 0 to period so that the formula - can yield 0 and 100. + line 在 0 到 100 之间 oscillate。这意味着距离最近 highest 或 lowest 的 + "distance" 必须从 0 到 period,公式才能产出 0 与 100。 - Hence the lookback period is period + 1, because the current bar is also - taken into account. And therefore this indicator needs an effective - lookback period of period + 1. + 因此 lookback period 是 period + 1,因为当前 bar 也参与计算。 See: - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:aroon + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(AroonUp, period=14) ''' _up = True @@ -98,23 +104,34 @@ def __init__(self): class AroonDown(_AroonBase): ''' - This is the AroonDown from the indicator AroonUpDown developed by Tushar - Chande in 1995. + Tushar Chande 于 1995 年开发的 AroonUpDown indicator 中的 AroonDown。 + + Args: + period: 回看周期。 + upperband: 绘图时的上轨参考线。 + lowerband: 绘图时的下轨参考线。 + + Returns: + AroonDown: 输出 ``aroondown`` line 的 indicator。 Formula: - down = 100 * (period - distance to lowest low) / period Note: - The lines oscillate between 0 and 100. That means that the "distance" to - the last highest or lowest must go from 0 to period so that the formula - can yield 0 and 100. + line 在 0 到 100 之间 oscillate。这意味着距离最近 highest 或 lowest 的 + "distance" 必须从 0 到 period,公式才能产出 0 与 100。 - Hence the lookback period is period + 1, because the current bar is also - taken into account. And therefore this indicator needs an effective - lookback period of period + 1. + 因此 lookback period 是 period + 1,因为当前 bar 也参与计算。 See: - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:aroon + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(AroonDown, period=14) ''' _down = True @@ -128,42 +145,66 @@ def __init__(self): class AroonUpDown(AroonUp, AroonDown): ''' - Developed by Tushar Chande in 1995. + Tushar Chande 于 1995 年开发的 AroonUpDown。 + + 它通过计算给定周期内最近 high/low 的距离(AroonUp/AroonDown),尝试判断趋势是否存在。 + + Args: + period: 回看周期。 + upperband: 绘图时的上轨参考线。 + lowerband: 绘图时的下轨参考线。 - It tries to determine if a trend exists or not by calculating how far away - within a given period the last highs/lows are (AroonUp/AroonDown) + Returns: + AroonUpDown: 输出 ``aroonup`` 与 ``aroondown`` line 的 indicator。 Formula: - up = 100 * (period - distance to highest high) / period - down = 100 * (period - distance to lowest low) / period Note: - The lines oscillate between 0 and 100. That means that the "distance" to - the last highest or lowest must go from 0 to period so that the formula - can yield 0 and 100. + line 在 0 到 100 之间 oscillate。这意味着距离最近 highest 或 lowest 的 + "distance" 必须从 0 到 period,公式才能产出 0 与 100。 - Hence the lookback period is period + 1, because the current bar is also - taken into account. And therefore this indicator needs an effective - lookback period of period + 1. + 因此 lookback period 是 period + 1,因为当前 bar 也参与计算。 See: - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:aroon + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(AroonUpDown, period=14) ''' alias = ('AroonIndicator',) class AroonOscillator(_AroonBase): ''' - It is a variation of the AroonUpDown indicator which shows the current - difference between the AroonUp and AroonDown value, trying to present a - visualization which indicates which is stronger (greater than 0 -> AroonUp - and less than 0 -> AroonDown) + AroonUpDown 的变体,显示 AroonUp 与 AroonDown 当前差值,用于直观看出哪一侧更强 + (大于 0 表示 AroonUp 更强,小于 0 表示 AroonDown 更强)。 + + Args: + period: 回看周期。 + upperband: 绘图时的上轨参考线。 + lowerband: 绘图时的下轨参考线。 + + Returns: + AroonOscillator: 输出 ``aroonosc`` line 的 indicator。 Formula: - aroonosc = aroonup - aroondown See: - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:aroon + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(AroonOscillator, period=14) ''' _up = True _down = True @@ -186,12 +227,28 @@ def __init__(self): class AroonUpDownOscillator(AroonUpDown, AroonOscillator): ''' - Presents together the indicators AroonUpDown and AroonOsc + 同时呈现 AroonUpDown 与 AroonOsc 的 indicator。 + + Args: + period: 回看周期。 + upperband: 绘图时的上轨参考线。 + lowerband: 绘图时的下轨参考线。 + + Returns: + AroonUpDownOscillator: 输出 AroonUpDown 与 AroonOsc 相关 line 的 + indicator。 Formula: (None, uses the aforementioned indicators) See: - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:aroon + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(AroonUpDownOscillator, period=14) ''' alias = ('AroonUpDownOsc',) diff --git a/backtrader/indicators/atr.py b/backtrader/indicators/atr.py index c6fd72e20..d534214ae 100644 --- a/backtrader/indicators/atr.py +++ b/backtrader/indicators/atr.py @@ -26,17 +26,29 @@ class TrueHigh(Indicator): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"* for the ATR + J. Welles Wilder, Jr. 于 1978 年在 *"New Concepts in Technical Trading + Systems"* 中为 ATR 定义的 TrueHigh。 - Records the "true high" which is the maximum of today's high and - yesterday's close + 记录 "true high",即今日 high 与昨日 close 的较大值。 + + Args: + data: 含有 high 与 close line 的数据源。 + + Returns: + TrueHigh: 输出 ``truehigh`` line 的 indicator。 Formula: - truehigh = max(high, close_prev) See: - http://en.wikipedia.org/wiki/Average_true_range + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(TrueHigh) ''' lines = ('truehigh',) @@ -47,17 +59,29 @@ def __init__(self): class TrueLow(Indicator): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"* for the ATR + J. Welles Wilder, Jr. 于 1978 年在 *"New Concepts in Technical Trading + Systems"* 中为 ATR 定义的 TrueLow。 - Records the "true low" which is the minimum of today's low and - yesterday's close + 记录 "true low",即今日 low 与昨日 close 的较小值。 + + Args: + data: 含有 low 与 close line 的数据源。 + + Returns: + TrueLow: 输出 ``truelow`` line 的 indicator。 Formula: - truelow = min(low, close_prev) See: - http://en.wikipedia.org/wiki/Average_true_range + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(TrueLow) ''' lines = ('truelow',) @@ -68,21 +92,34 @@ def __init__(self): class TrueRange(Indicator): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book New Concepts in - Technical Trading Systems. + J. Welles Wilder, Jr. 于 1978 年在 *New Concepts in Technical Trading + Systems* 中定义的 TrueRange。 + + 它会纳入前一根 bar 的 close;当隔夜跳空使真实区间大于日内 + High-Low 时,可以反映更完整的 range。 + + Args: + data: 含有 high/low/close line 的数据源。 + + Returns: + TrueRange: 输出 ``tr`` line 的 indicator。 Formula: - max(high - low, abs(high - prev_close), abs(prev_close - low) - which can be simplified to + 可简化为: - max(high, prev_close) - min(low, prev_close) See: - http://en.wikipedia.org/wiki/Average_true_range - The idea is to take the previous close into account to calculate the range - if it yields a larger range than the daily range (High - Low) + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(TrueRange) ''' alias = ('TR',) @@ -95,17 +132,31 @@ def __init__(self): class AverageTrueRange(Indicator): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"*. + J. Welles Wilder, Jr. 于 1978 年在 *"New Concepts in Technical Trading + Systems"* 中定义的 Average True Range。 - The idea is to take the close into account to calculate the range if it - yields a larger range than the daily range (High - Low) + 它会纳入 close 来计算 range;当真实区间大于日内 High-Low 时,可以反映更完整的 + 波动范围。 + + Args: + period: 平滑 TrueRange 的周期。 + movav: 用于平滑的 Moving Average 类型。 + + Returns: + AverageTrueRange: 输出 ``atr`` line 的 indicator。 Formula: - SmoothedMovingAverage(TrueRange, period) See: - http://en.wikipedia.org/wiki/Average_true_range + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(AverageTrueRange, period=14) ''' alias = ('ATR',) diff --git a/backtrader/indicators/awesomeoscillator.py b/backtrader/indicators/awesomeoscillator.py index 40b5ac16d..7097ac5b9 100644 --- a/backtrader/indicators/awesomeoscillator.py +++ b/backtrader/indicators/awesomeoscillator.py @@ -30,10 +30,16 @@ class AwesomeOscillator(bt.Indicator): ''' - Awesome Oscillator (AO) is a momentum indicator reflecting the precise - changes in the market driving force which helps to identify the trend’s - strength up to the points of formation and reversal. + Awesome Oscillator(AO)是 momentum indicator,用于反映市场驱动力的细微变化, + 帮助识别趋势强度以及形成/反转位置。 + Args: + fast: 快速 SMA 周期。 + slow: 慢速 SMA 周期。 + movav: 使用的 Moving Average 类型。 + + Returns: + AwesomeOscillator: 输出 ``ao`` line 的 indicator。 Formula: - median price = (high + low) / 2 @@ -43,6 +49,12 @@ class AwesomeOscillator(bt.Indicator): - https://www.metatrader5.com/en/terminal/help/indicators/bw_indicators/awesome - https://www.ifcmarkets.com/en/ntx-indicators/awesome-oscillator + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(AwesomeOscillator) ''' alias = ('AwesomeOsc', 'AO') lines = ('ao',) diff --git a/backtrader/indicators/basicops.py b/backtrader/indicators/basicops.py index b4e3c6b24..d22723b8f 100644 --- a/backtrader/indicators/basicops.py +++ b/backtrader/indicators/basicops.py @@ -32,10 +32,9 @@ class PeriodN(Indicator): ''' - Base class for indicators which take a period (__init__ has to be called - either via super or explicitly) + 接受 ``period`` 参数的 indicator 基类,用于统一最小周期设置。 - This class has no defined lines + 该类不定义 line;子类需要通过 ``super`` 或显式调用其 ``__init__``。 ''' params = (('period', 1),) @@ -46,13 +45,10 @@ def __init__(self): class OperationN(PeriodN): ''' - Calculates "func" for a given period - - Serves as a base for classes that work with a period and can express the - logic in a callable object + 按给定周期计算 ``func`` 的基类,用于逻辑可由 callable 表达的周期型 indicator。 Note: - Base classes must provide a "func" attribute which is a callable + 子类必须提供可调用的 ``func`` 属性。 Formula: - line = func(data, period) @@ -72,16 +68,14 @@ def once(self, start, end): class BaseApplyN(OperationN): ''' - Base class for ApplyN and others which may take a ``func`` as a parameter - but want to define the lines in the indicator. + ApplyN 及类似类的基类,用于接收 ``func`` 参数并由 indicator 自身定义 line。 - Calculates ``func`` for a given period where func is given as a parameter, - aka named argument or ``kwarg`` + ``func`` 通过具名参数(``kwarg``)传入,并按给定周期计算。 Formula: - lines[0] = func(data, period) - Any extra lines defined beyond the first (index 0) are not calculated + 第 1 条 line(索引 0)之外的额外 line 不会自动计算。 ''' params = (('func', None),) @@ -92,22 +86,49 @@ def __init__(self): class ApplyN(BaseApplyN): ''' - Calculates ``func`` for a given period + 按给定周期计算 ``func``。 + + Args: + period: 计算窗口长度。 + func: 接收窗口数据并返回结果的 callable。 + + Returns: + ApplyN: 输出 ``apply`` line 的 indicator。 Formula: - line = func(data, period) + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(ApplyN, period=5, func=max) ''' lines = ('apply',) class Highest(OperationN): ''' - Calculates the highest value for the data in a given period + 计算给定周期内 data 的最高值。 - Uses the built-in ``max`` for the calculation + 使用内置 ``max`` 计算。 + + Args: + period: 计算窗口长度。 + + Returns: + Highest: 输出 ``highest`` line 的 indicator。 Formula: - highest = max(data, period) + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(Highest, period=14) ''' alias = ('MaxN',) lines = ('highest',) @@ -116,12 +137,25 @@ class Highest(OperationN): class Lowest(OperationN): ''' - Calculates the lowest value for the data in a given period + 计算给定周期内 data 的最低值。 - Uses the built-in ``min`` for the calculation + 使用内置 ``min`` 计算。 + + Args: + period: 计算窗口长度。 + + Returns: + Lowest: 输出 ``lowest`` line 的 indicator。 Formula: - lowest = min(data, period) + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(Lowest, period=14) ''' alias = ('MinN',) lines = ('lowest',) @@ -130,20 +164,33 @@ class Lowest(OperationN): class ReduceN(OperationN): ''' - Calculates the Reduced value of the ``period`` data points applying - ``function`` + 对 ``period`` 个 data 点应用 ``function``,计算 reduce 结果。 + + 使用内置 ``reduce`` 以及子类定义的 ``func`` 完成计算。 - Uses the built-in ``reduce`` for the calculation plus the ``func`` that - subclassess define + Args: + function: 传给 ``functools.reduce`` 的二元 callable。 + period: 计算窗口长度。 + initializer: 可选的 reduce 初始值。 + + Returns: + ReduceN: 输出 ``reduced`` line 的 indicator。 Formula: - reduced = reduce(function(data, period)), initializer=initializer) Notes: - - In order to mimic the python ``reduce``, this indicator takes a - ``function`` non-named argument as the 1st argument, unlike other - Indicators which take only named arguments + - 为模拟 Python ``reduce``,该 indicator 将 ``function`` 作为第 1 个非具名参数, + 不同于大多数只接受具名参数的 indicator。 + + --- + 交互界面使用示范: + + >>> import operator + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(ReduceN, operator.add, period=5) ''' lines = ('reduced',) func = functools.reduce @@ -160,13 +207,25 @@ def __init__(self, function, **kwargs): class SumN(OperationN): ''' - Calculates the Sum of the data values over a given period + 计算给定周期内 data 值的总和。 + + 使用 ``math.fsum`` 而不是内置 ``sum``,以降低精度误差。 - Uses ``math.fsum`` for the calculation rather than the built-in ``sum`` to - avoid precision errors + Args: + period: 计算窗口长度。 + + Returns: + SumN: 输出 ``sumn`` line 的 indicator。 Formula: - sumn = sum(data, period) + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(SumN, period=10) ''' lines = ('sumn',) func = math.fsum @@ -174,13 +233,25 @@ class SumN(OperationN): class AnyN(OperationN): ''' - Has a value of ``True`` (stored as ``1.0`` in the lines) if *any* of the - values in the ``period`` evaluates to non-zero (ie: ``True``) + 如果 ``period`` 内任意值为非零(即 ``True``),line 值为 ``True``(存为 ``1.0``)。 + + 使用内置 ``any`` 计算。 + + Args: + period: 计算窗口长度。 - Uses the built-in ``any`` for the calculation + Returns: + AnyN: 输出 ``anyn`` line 的 indicator。 Formula: - anyn = any(data, period) + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(AnyN, period=3) ''' lines = ('anyn',) func = any @@ -188,13 +259,25 @@ class AnyN(OperationN): class AllN(OperationN): ''' - Has a value of ``True`` (stored as ``1.0`` in the lines) if *all* of the - values in the ``period`` evaluates to non-zero (ie: ``True``) + 如果 ``period`` 内所有值都为非零(即 ``True``),line 值为 ``True``(存为 ``1.0``)。 + + 使用内置 ``all`` 计算。 + + Args: + period: 计算窗口长度。 - Uses the built-in ``all`` for the calculation + Returns: + AllN: 输出 ``alln`` line 的 indicator。 Formula: - alln = all(data, period) + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(AllN, period=3) ''' lines = ('alln',) func = all @@ -202,12 +285,17 @@ class AllN(OperationN): class FindFirstIndex(OperationN): ''' - Returns the index of the last data that satisfies equality with the - condition generated by the parameter _evalfunc + 返回与 ``_evalfunc`` 生成条件相等的第一个 data 的回看索引。 + + Args: + period: 搜索窗口长度。 + _evalfunc: 用于从窗口数据中生成目标值的 callable。 + + Returns: + FindFirstIndex: 输出 ``index`` line 的 indicator。 Note: - Returned indexes look backwards. 0 is the current index and 1 is - the previous bar. + 返回索引按回看方向计算。0 表示当前 bar,1 表示前一根 bar。 Formula: - index = first for which data[index] == _evalfunc(data) @@ -222,40 +310,69 @@ def func(self, iterable): class FindFirstIndexHighest(FindFirstIndex): ''' - Returns the index of the first data that is the highest in the period + 返回周期内第一个最高值的回看索引。 + + Args: + period: 搜索窗口长度。 + + Returns: + FindFirstIndexHighest: 输出 ``index`` line 的 indicator。 Note: - Returned indexes look backwards. 0 is the current index and 1 is - the previous bar. + 返回索引按回看方向计算。0 表示当前 bar,1 表示前一根 bar。 Formula: - index = index of first data which is the highest + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(FindFirstIndexHighest, period=10) ''' params = (('_evalfunc', max),) class FindFirstIndexLowest(FindFirstIndex): ''' - Returns the index of the first data that is the lowest in the period + 返回周期内第一个最低值的回看索引。 + + Args: + period: 搜索窗口长度。 + + Returns: + FindFirstIndexLowest: 输出 ``index`` line 的 indicator。 Note: - Returned indexes look backwards. 0 is the current index and 1 is - the previous bar. + 返回索引按回看方向计算。0 表示当前 bar,1 表示前一根 bar。 Formula: - index = index of first data which is the lowest + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(FindFirstIndexLowest, period=10) ''' params = (('_evalfunc', min),) class FindLastIndex(OperationN): ''' - Returns the index of the last data that satisfies equality with the - condition generated by the parameter _evalfunc + 返回与 ``_evalfunc`` 生成条件相等的最后一个 data 的回看索引。 + + Args: + period: 搜索窗口长度。 + _evalfunc: 用于从窗口数据中生成目标值的 callable。 + + Returns: + FindLastIndex: 输出 ``index`` line 的 indicator。 Note: - Returned indexes look backwards. 0 is the current index and 1 is - the previous bar. + 返回索引按回看方向计算。0 表示当前 bar,1 表示前一根 bar。 Formula: - index = last for which data[index] == _evalfunc(data) @@ -266,54 +383,89 @@ class FindLastIndex(OperationN): def func(self, iterable): m = self.p._evalfunc(iterable) index = next(i for i, v in enumerate(iterable) if v == m) - # The iterable goes from 0 -> period - 1. If the last element - # which is the current bar is returned and without the -1 then - # period - index = 1 ... and must be zero! + # iterable 范围为 0 -> period - 1。如果返回最后一个元素(当前 bar) + # 且不减 1,则 period - index = 1;这里必须为 0。 return self.p.period - index - 1 class FindLastIndexHighest(FindLastIndex): ''' - Returns the index of the last data that is the highest in the period + 返回周期内最后一个最高值的回看索引。 + + Args: + period: 搜索窗口长度。 + + Returns: + FindLastIndexHighest: 输出 ``index`` line 的 indicator。 Note: - Returned indexes look backwards. 0 is the current index and 1 is - the previous bar. + 返回索引按回看方向计算。0 表示当前 bar,1 表示前一根 bar。 Formula: - index = index of last data which is the highest + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(FindLastIndexHighest, period=10) ''' params = (('_evalfunc', max),) class FindLastIndexLowest(FindLastIndex): ''' - Returns the index of the last data that is the lowest in the period + 返回周期内最后一个最低值的回看索引。 + + Args: + period: 搜索窗口长度。 + + Returns: + FindLastIndexLowest: 输出 ``index`` line 的 indicator。 Note: - Returned indexes look backwards. 0 is the current index and 1 is - the previous bar. + 返回索引按回看方向计算。0 表示当前 bar,1 表示前一根 bar。 Formula: - index = index of last data which is the lowest + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(FindLastIndexLowest, period=10) ''' params = (('_evalfunc', min),) class Accum(Indicator): ''' - Cummulative sum of the data values + 计算 data 值的累计和。 + + Args: + seed: 累计初始值。 + + Returns: + Accum: 输出 ``accum`` line 的 indicator。 Formula: - accum += data + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(Accum, seed=0.0) ''' alias = ('CumSum', 'CumulativeSum',) lines = ('accum',) params = (('seed', 0.0),) - # xxxstart methods use the seed (starting value) and passed data to - # construct the first value keeping the minperiod to 1 since no - # initial look-back value is needed + # xxxstart 方法使用 seed(起始值)和传入 data 构造第一个值。 + # 因为不需要初始回看值,所以 minperiod 保持为 1。 def nextstart(self): self.line[0] = self.p.seed + self.data[0] @@ -340,13 +492,26 @@ def once(self, start, end): class Average(PeriodN): ''' - Averages a given data arithmetically over a period + 计算给定 data 在指定周期内的算术平均值。 + + Args: + period: 计算窗口长度。 + + Returns: + Average: 输出 ``av`` line 的 indicator。 Formula: - av = data(period) / period See also: - https://en.wikipedia.org/wiki/Arithmetic_mean + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(Average, period=10) ''' alias = ('ArithmeticMean', 'Mean',) lines = ('av',) @@ -366,16 +531,29 @@ def once(self, start, end): class ExponentialSmoothing(Average): ''' - Averages a given data over a period using exponential smoothing + 使用 exponential smoothing 计算给定 data 在指定周期内的平均值。 - A regular ArithmeticMean (Average) is used as the seed value considering - the first period values of data + 初始种子值使用前 ``period`` 个 data 的普通 ArithmeticMean(Average)。 + + Args: + period: 计算窗口长度。 + alpha: 平滑因子;未提供时使用 EMA 默认值。 + + Returns: + ExponentialSmoothing: 输出 ``av`` line 的 indicator。 Formula: - av = prev * (1 - alpha) + data * alpha See also: - https://en.wikipedia.org/wiki/Exponential_smoothing + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(ExponentialSmoothing, period=10) ''' alias = ('ExpSmoothing',) params = (('alpha', None),) @@ -383,21 +561,21 @@ class ExponentialSmoothing(Average): def __init__(self): self.alpha = self.p.alpha if self.alpha is None: - self.alpha = 2.0 / (1.0 + self.p.period) # def EMA value + self.alpha = 2.0 / (1.0 + self.p.period) # 默认 EMA 值 self.alpha1 = 1.0 - self.alpha super(ExponentialSmoothing, self).__init__() def nextstart(self): - # Fetch the seed value from the base class calculation + # 从基类计算中获取种子值 super(ExponentialSmoothing, self).next() def next(self): self.line[0] = self.line[-1] * self.alpha1 + self.data[0] * self.alpha def oncestart(self, start, end): - # Fetch the seed value from the base class calculation + # 从基类计算中获取种子值 super(ExponentialSmoothing, self).once(start, end) def once(self, start, end): @@ -406,7 +584,7 @@ def once(self, start, end): alpha = self.alpha alpha1 = self.alpha1 - # Seed value from SMA calculated with the call to oncestart + # 种子值来自 oncestart 调用计算出的 SMA prev = larray[start - 1] for i in range(start, end): larray[i] = prev = prev * alpha1 + darray[i] * alpha @@ -414,28 +592,40 @@ def once(self, start, end): class ExponentialSmoothingDynamic(ExponentialSmoothing): ''' - Averages a given data over a period using exponential smoothing + 使用动态 alpha 的 exponential smoothing 计算给定 data 的平均值。 - A regular ArithmeticMean (Average) is used as the seed value considering - the first period values of data + 初始种子值使用前 ``period`` 个 data 的普通 ArithmeticMean(Average)。 + + Args: + period: 计算窗口长度。 + alpha: 可动态变化的 alpha line。 + + Returns: + ExponentialSmoothingDynamic: 输出 ``av`` line 的 indicator。 Note: - - alpha is an array of values which can be calculated dynamically + - alpha 是一个可动态计算的值数组。 Formula: - av = prev * (1 - alpha) + data * alpha See also: - https://en.wikipedia.org/wiki/Exponential_smoothing + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(ExponentialSmoothingDynamic, period=10) ''' alias = ('ExpSmoothingDynamic',) def __init__(self): super(ExponentialSmoothingDynamic, self).__init__() - # Hack: alpha is a "line" and carries a minperiod which is not being - # considered because this indicator makes no line assignment. It has - # therefore to be considered manually + # Hack: alpha 是一个 "line",带有 minperiod;由于该 indicator 不进行 line + # 赋值,minperiod 不会被自动纳入,因此需要手动处理。 minperioddiff = max(0, self.alpha._minperiod - self.p.period) self.lines[0].incminperiod(minperioddiff) @@ -449,7 +639,7 @@ def once(self, start, end): alpha = self.alpha.array alpha1 = self.alpha1.array - # Seed value from SMA calculated with the call to oncestart + # 种子值来自 oncestart 调用计算出的 SMA prev = larray[start - 1] for i in range(start, end): larray[i] = prev = prev * alpha1[i] + darray[i] * alpha[i] @@ -457,18 +647,32 @@ def once(self, start, end): class WeightedAverage(PeriodN): ''' - Calculates the weighted average of the given data over a period + 计算给定 data 在指定周期内的加权平均值。 - The default weights (if none are provided) are linear to assigne more - weight to the most recent data + 如未提供 weights,默认权重通常由具体子类设置,用于给近期数据更高权重。 - The result will be multiplied by a given "coef" + 结果会乘以给定 ``coef``。 + + Args: + period: 计算窗口长度。 + coef: 结果缩放系数。 + weights: 应用于窗口数据的权重序列。 + + Returns: + WeightedAverage: 输出 ``av`` line 的 indicator。 Formula: - av = coef * sum(mul(data, period), weights) See: - https://en.wikipedia.org/wiki/Weighted_arithmetic_mean + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(WeightedAverage, period=3, weights=(1.0, 2.0, 3.0)) ''' alias = ('AverageWeighted',) lines = ('av',) diff --git a/backtrader/indicators/bollinger.py b/backtrader/indicators/bollinger.py index 8f95474ad..6111041fa 100644 --- a/backtrader/indicators/bollinger.py +++ b/backtrader/indicators/bollinger.py @@ -26,8 +26,17 @@ class BollingerBands(Indicator): ''' - Defined by John Bollinger in the 80s. It measures volatility by defining - upper and lower bands at distance x standard deviations + John Bollinger 在 20 世纪 80 年代定义的 Bollinger Bands 指标。 + + 通过在若干倍 standard deviation 距离上定义上下轨来衡量 volatility。 + + Args: + period: Moving Average 和 Standard Deviation 的周期。 + devfactor: standard deviation 乘数。 + movav: 使用的 Moving Average 类型。 + + Returns: + BollingerBands: 输出 ``mid``、``top``、``bot`` line 的 indicator。 Formula: - midband = SimpleMovingAverage(close, period) @@ -36,6 +45,13 @@ class BollingerBands(Indicator): See: - http://en.wikipedia.org/wiki/Bollinger_Bands + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(BollingerBands, period=20, devfactor=2.0) ''' alias = ('BBands',) @@ -66,10 +82,25 @@ def __init__(self): class BollingerBandsPct(BollingerBands): ''' - Extends the Bollinger Bands with a Percentage line + 扩展 ``BollingerBands``,增加 Percentage line。 + + Args: + period: Moving Average 和 Standard Deviation 的周期。 + devfactor: standard deviation 乘数。 + movav: 使用的 Moving Average 类型。 + + Returns: + BollingerBandsPct: 在 Bollinger Bands 基础上额外输出 ``pctb`` line。 + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(BollingerBandsPct) ''' lines = ('pctb',) - plotlines = dict(pctb=dict(_name='%B')) # display the line as %B on chart + plotlines = dict(pctb=dict(_name='%B')) # 图上把该 line 显示为 %B def __init__(self): super(BollingerBandsPct, self).__init__() diff --git a/backtrader/indicators/cci.py b/backtrader/indicators/cci.py index ab07f61f8..59ddd41e1 100644 --- a/backtrader/indicators/cci.py +++ b/backtrader/indicators/cci.py @@ -26,9 +26,18 @@ class CommodityChannelIndex(Indicator): ''' - Introduced by Donald Lambert in 1980 to measure variations of the - "typical price" (see below) from its mean to identify extremes and - reversals + Donald Lambert 于 1980 年提出的 Commodity Channel Index,用于衡量 + "typical price" 相对均值的偏离,以识别极端状态和反转。 + + Args: + period: 计算 typical price 均值与平均偏差的周期。 + factor: 标准化偏离值的缩放因子。 + movav: 用于 typical price 均值的 Moving Average 类型。 + upperband: 绘图时的上轨参考线。 + lowerband: 绘图时的下轨参考线。 + + Returns: + CommodityChannelIndex: 输出 ``cci`` line 的 indicator。 Formula: - tp = typical_price = (high + low + close) / 3 @@ -39,6 +48,13 @@ class CommodityChannelIndex(Indicator): See: - https://en.wikipedia.org/wiki/Commodity_channel_index + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(CommodityChannelIndex, period=20) ''' alias = ('CCI',) diff --git a/backtrader/indicators/contrib/vortex.py b/backtrader/indicators/contrib/vortex.py index fbeab5c64..05f57420f 100644 --- a/backtrader/indicators/contrib/vortex.py +++ b/backtrader/indicators/contrib/vortex.py @@ -28,10 +28,25 @@ class Vortex(bt.Indicator): - ''' + '''Vortex 指标。 + + 该指标计算 ``vi_plus`` 与 ``vi_minus`` 两条线,用于衡量正向/反向趋势运动。 + + Args: + - ``period`` (default: ``14``): 计算 vortex movement 和 true range 求和时 + 使用的周期。 + + Returns: + Vortex: 输出 ``vi_plus`` 和 ``vi_minus`` 两条 indicator lines。 + + --- + 交互界面使用示范例子: + + - 在 strategy 中调用 ``bt.ind.Vortex(self.data, period=14)``,即可得到 + ``vi_plus`` / ``vi_minus`` 两条线用于判断趋势方向。 + See: - http://www.vortexindicator.com/VFX_VORTEX.PDF - ''' lines = ('vi_plus', 'vi_minus',) diff --git a/backtrader/indicators/crossover.py b/backtrader/indicators/crossover.py index 86c0f5de6..24b8b14bb 100644 --- a/backtrader/indicators/crossover.py +++ b/backtrader/indicators/crossover.py @@ -26,19 +26,32 @@ class NonZeroDifference(Indicator): ''' - Keeps track of the difference between two data inputs skipping, memorizing - the last non zero value if the current difference is zero + 跟踪两个输入 data 的差值;当前差值为 0 时,沿用上一个非 0 差值。 + + Args: + data0: 第一个 data。 + data1: 第二个 data。 + + Returns: + NonZeroDifference: 输出 ``nzd`` line 的 indicator。 Formula: - diff = data - data1 - nzd = diff if diff else diff(-1) + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(NonZeroDifference) ''' - _mindatas = 2 # requires two (2) data sources + _mindatas = 2 # 需要两个 data source alias = ('NZD',) lines = ('nzd',) def nextstart(self): - self.l.nzd[0] = self.data0[0] - self.data1[0] # seed value + self.l.nzd[0] = self.data0[0] - self.data1[0] # 种子值 def next(self): d = self.data0[0] - self.data1[0] @@ -60,6 +73,8 @@ def once(self, start, end): class _CrossBase(Indicator): + '''交叉判断的基类,用于实现向上/向下穿越的共同逻辑。''' + _mindatas = 2 lines = ('cross',) @@ -70,10 +85,10 @@ def __init__(self): nzd = NonZeroDifference(self.data0, self.data1) if self._crossup: - before = nzd(-1) < 0.0 # data0 was below or at 0 + before = nzd(-1) < 0.0 # data0 之前在下方或等于 0 after = self.data0 > self.data1 else: - before = nzd(-1) > 0.0 # data0 was above or at 0 + before = nzd(-1) > 0.0 # data0 之前在上方或等于 0 after = self.data0 < self.data1 self.lines.cross = And(before, after) @@ -81,49 +96,87 @@ def __init__(self): class CrossUp(_CrossBase): ''' - This indicator gives a signal if the 1st provided data crosses over the 2nd - indicator upwards + 当第一个 data 向上穿越第二个 data 时给出信号。 - It does need to look into the current time index (0) and the previous time - index (-1) of both the 1st and 2nd data + Args: + data0: 第一个 data。 + data1: 第二个 data。 + + Returns: + CrossUp: 输出 ``cross`` line 的向上穿越 indicator。 + + 会查看两个 data 的当前索引 ``0`` 和前一索引 ``-1``。 Formula: - diff = data - data1 - upcross = last_non_zero_diff < 0 and data0(0) > data1(0) + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(CrossUp) ''' _crossup = True class CrossDown(_CrossBase): ''' - This indicator gives a signal if the 1st provided data crosses over the 2nd - indicator upwards + 当第一个 data 向下穿越第二个 data 时给出信号。 - It does need to look into the current time index (0) and the previous time - index (-1) of both the 1st and 2nd data + Args: + data0: 第一个 data。 + data1: 第二个 data。 + + Returns: + CrossDown: 输出 ``cross`` line 的向下穿越 indicator。 + + 会查看两个 data 的当前索引 ``0`` 和前一索引 ``-1``。 Formula: - diff = data - data1 - downcross = last_non_zero_diff > 0 and data0(0) < data1(0) + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(CrossDown) ''' _crossup = False class CrossOver(Indicator): ''' - This indicator gives a signal if the provided datas (2) cross up or down. + 当两个 data 向上或向下穿越时给出信号。 - 1.0 if the 1st data crosses the 2nd data upwards - -1.0 if the 1st data crosses the 2nd data downwards - It does need to look into the current time index (0) and the previous time - index (-1) of both the 1t and 2nd data + Args: + data0: 第一个 data。 + data1: 第二个 data。 + + Returns: + CrossOver: 输出 ``crossover`` line 的 indicator;向上穿越为 ``1.0``, + 向下穿越为 ``-1.0``。 + + 会查看两个 data 的当前索引 ``0`` 和前一索引 ``-1``。 Formula: - diff = data - data1 - upcross = last_non_zero_diff < 0 and data0(0) > data1(0) - downcross = last_non_zero_diff > 0 and data0(0) < data1(0) - crossover = upcross - downcross + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(CrossOver) ''' _mindatas = 2 diff --git a/backtrader/indicators/dema.py b/backtrader/indicators/dema.py index 4f293a1e8..233830aaf 100644 --- a/backtrader/indicators/dema.py +++ b/backtrader/indicators/dema.py @@ -27,17 +27,31 @@ class DoubleExponentialMovingAverage(MovingAverageBase): ''' - DEMA was first time introduced in 1994, in the article "Smoothing Data with - Faster Moving Averages" by Patrick G. Mulloy in "Technical Analysis of - Stocks & Commodities" magazine. + Patrick G. Mulloy 于 1994 年在 *Technical Analysis of Stocks & Commodities* + 的文章 "Smoothing Data with Faster Moving Averages" 中首次介绍 DEMA。 - It attempts to reduce the inherent lag associated to Moving Averages + 它尝试降低 Moving Average 固有的滞后。 + + Args: + period: EMA 计算周期。 + _movav: 用于计算的 Moving Average 类型。 + + Returns: + DoubleExponentialMovingAverage: 输出 ``dema`` line 的 Moving Average + indicator。 Formula: - dema = (2.0 - ema(data, period) - ema(ema(data, period), period) See: (None) + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(DoubleExponentialMovingAverage, period=30) ''' alias = ('DEMA', 'MovingAverageDoubleExponential',) @@ -54,11 +68,18 @@ def __init__(self): class TripleExponentialMovingAverage(MovingAverageBase): ''' - TEMA was first time introduced in 1994, in the article "Smoothing Data with - Faster Moving Averages" by Patrick G. Mulloy in "Technical Analysis of - Stocks & Commodities" magazine. + Patrick G. Mulloy 于 1994 年在 *Technical Analysis of Stocks & Commodities* + 的文章 "Smoothing Data with Faster Moving Averages" 中首次介绍 TEMA。 - It attempts to reduce the inherent lag associated to Moving Averages + 它尝试进一步降低 Moving Average 固有的滞后。 + + Args: + period: EMA 计算周期。 + _movav: 用于计算的 Moving Average 类型。 + + Returns: + TripleExponentialMovingAverage: 输出 ``tema`` line 的 Moving Average + indicator。 Formula: - ema1 = ema(data, period) @@ -68,6 +89,13 @@ class TripleExponentialMovingAverage(MovingAverageBase): See: (None) + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(TripleExponentialMovingAverage, period=30) ''' alias = ('TEMA', 'MovingAverageTripleExponential',) diff --git a/backtrader/indicators/deviation.py b/backtrader/indicators/deviation.py index 33ed54826..ecb00fc50 100644 --- a/backtrader/indicators/deviation.py +++ b/backtrader/indicators/deviation.py @@ -26,16 +26,22 @@ class StandardDeviation(Indicator): ''' - Calculates the standard deviation of the passed data for a given period + 计算传入 data 在指定周期内的 standard deviation。 + + Args: + period: 计算周期。 + movav: 用于均值计算的 Moving Average 类型。 + safepow: 是否对平方根入参取绝对值,以规避浮点表示导致的负值。 + + Returns: + StandardDeviation: 输出 ``stddev`` line 的 indicator。 Note: - - If 2 datas are provided as parameters, the 2nd is considered to be the - mean of the first + - 如果传入 2 个 data,第 2 个会被视为第 1 个 data 的均值。 - - ``safepow`` (default: False) If this parameter is True, the standard - deviation will be calculated as pow(abs(meansq - sqmean), 0.5) to safe - guard for possible negative results of ``meansq - sqmean`` caused by - the floating point representation. + - ``safepow`` 为 True 时,standard deviation 会以 + ``pow(abs(meansq - sqmean), 0.5)`` 计算,以防浮点表示使 + ``meansq - sqmean`` 出现微小负值。 Formula: - meansquared = SimpleMovingAverage(pow(data, 2), period) @@ -44,6 +50,13 @@ class StandardDeviation(Indicator): See: - http://en.wikipedia.org/wiki/Standard_deviation + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(StandardDeviation, period=20) ''' alias = ('StdDev',) @@ -71,13 +84,19 @@ def __init__(self): class MeanDeviation(Indicator): - '''MeanDeviation (alias MeanDev) + '''MeanDeviation(别名 MeanDev)。 - Calculates the Mean Deviation of the passed data for a given period + 计算传入 data 在指定周期内的 Mean Deviation。 + + Args: + period: 计算周期。 + movav: 用于均值与平均偏差计算的 Moving Average 类型。 + + Returns: + MeanDeviation: 输出 ``meandev`` line 的 indicator。 Note: - - If 2 datas are provided as parameters, the 2nd is considered to be the - mean of the first + - 如果传入 2 个 data,第 2 个会被视为第 1 个 data 的均值。 Formula: - mean = MovingAverage(data, period) (or provided mean) @@ -86,6 +105,13 @@ class MeanDeviation(Indicator): See: - https://en.wikipedia.org/wiki/Average_absolute_deviation + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(MeanDeviation, period=20) ''' alias = ('MeanDev',) diff --git a/backtrader/indicators/directionalmove.py b/backtrader/indicators/directionalmove.py index f52cd1468..52614a41d 100644 --- a/backtrader/indicators/directionalmove.py +++ b/backtrader/indicators/directionalmove.py @@ -26,17 +26,30 @@ class UpMove(Indicator): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"* as part of the Directional Move System to - calculate Directional Indicators. + J. Welles Wilder, Jr. 于 1978 年在 *"New Concepts in Technical Trading + Systems"* 中作为 Directional Move System 的一部分定义,用于计算 + Directional Indicator。 - Positive if the given data has moved higher than the previous day + 当给定 data 高于前一日时为正。 + + Args: + data: 用于比较的 line。 + + Returns: + UpMove: 输出 ``upmove`` line 的 indicator。 Formula: - upmove = data - data(-1) See: - https://en.wikipedia.org/wiki/Average_directional_movement_index + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(UpMove) ''' lines = ('upmove',) @@ -47,17 +60,30 @@ def __init__(self): class DownMove(Indicator): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"* as part of the Directional Move System to - calculate Directional Indicators. + J. Welles Wilder, Jr. 于 1978 年在 *"New Concepts in Technical Trading + Systems"* 中作为 Directional Move System 的一部分定义,用于计算 + Directional Indicator。 + + 当给定 data 低于前一日时为正。 - Positive if the given data has moved lower than the previous day + Args: + data: 用于比较的 line。 + + Returns: + DownMove: 输出 ``downmove`` line 的 indicator。 Formula: - downmove = data(-1) - data See: - https://en.wikipedia.org/wiki/Average_directional_movement_index + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(DownMove) ''' lines = ('downmove',) @@ -68,13 +94,9 @@ def __init__(self): class _DirectionalIndicator(Indicator): ''' - This class serves as the root base class for all "Directional Movement - System" related indicators, given that the calculations are first common - and then derived from the common calculations. + Directional Movement System 相关 indicator 的根基类,用于承载公共计算。 - It can calculate the +DI and -DI values (using kwargs as the hint as to - what to calculate) but doesn't assign them to lines. This is left for - sublcases of this class. + 它可根据参数提示计算 +DI 和 -DI,但不直接赋给 line;具体赋值由子类完成。 ''' params = (('period', 14), ('movav', MovAv.Smoothed)) @@ -110,18 +132,25 @@ def __init__(self, _plus=True, _minus=True): class DirectionalIndicator(_DirectionalIndicator): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"*. + J. Welles Wilder, Jr. 于 1978 年在 *"New Concepts in Technical Trading + Systems"* 中定义的 Directional Indicator。 + + 用于衡量趋势强度。 - Intended to measure trend strength + 该 indicator 显示 +DI、-DI: + - 使用 PlusDirectionalIndicator (PlusDI) 获取 +DI + - 使用 MinusDirectionalIndicator (MinusDI) 获取 -DI + - 使用 AverageDirectionalIndex (ADX) 获取 ADX + - 使用 AverageDirectionalIndexRating (ADXR) 获取 ADX、ADXR + - 使用 DirectionalMovementIndex (DMI) 获取 ADX、+DI、-DI + - 使用 DirectionalMovement (DM) 获取 ADX、ADXR、+DI、-DI - This indicator shows +DI, -DI: - - Use PlusDirectionalIndicator (PlusDI) to get +DI - - Use MinusDirectionalIndicator (MinusDI) to get -DI - - Use AverageDirectionalIndex (ADX) to get ADX - - Use AverageDirectionalIndexRating (ADXR) to get ADX, ADXR - - Use DirectionalMovementIndex (DMI) to get ADX, +DI, -DI - - Use DirectionalMovement (DM) to get ADX, ADXR, +DI, -DI + Args: + period: 计算周期。 + movav: 用于平滑的 Moving Average 类型。 + + Returns: + DirectionalIndicator: 输出 ``plusDI`` 与 ``minusDI`` line 的 indicator。 Formula: - upmove = high - high(-1) @@ -131,11 +160,17 @@ class DirectionalIndicator(_DirectionalIndicator): - +di = 100 * MovingAverage(+dm, period) / atr(period) - -di = 100 * MovingAverage(-dm, period) / atr(period) - The moving average used is the one originally defined by Wilder, - the SmoothedMovingAverage + 默认 Moving Average 使用 Wilder 原始定义中的 SmoothedMovingAverage。 See: - https://en.wikipedia.org/wiki/Average_directional_movement_index + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(DirectionalIndicator) ''' alias = ('DI',) lines = ('plusDI', 'minusDI',) @@ -149,18 +184,18 @@ def __init__(self): class PlusDirectionalIndicator(_DirectionalIndicator): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"*. + J. Welles Wilder, Jr. 于 1978 年定义的 +DI indicator。 + + 用于衡量趋势强度。 + + 该 indicator 显示 +DI。 - Intended to measure trend strength + Args: + period: 计算周期。 + movav: 用于平滑的 Moving Average 类型。 - This indicator shows +DI: - - Use MinusDirectionalIndicator (MinusDI) to get -DI - - Use Directional Indicator (DI) to get +DI, -DI - - Use AverageDirectionalIndex (ADX) to get ADX - - Use AverageDirectionalIndexRating (ADXR) to get ADX, ADXR - - Use DirectionalMovementIndex (DMI) to get ADX, +DI, -DI - - Use DirectionalMovement (DM) to get ADX, ADXR, +DI, -DI + Returns: + PlusDirectionalIndicator: 输出 ``plusDI`` line 的 indicator。 Formula: - upmove = high - high(-1) @@ -168,11 +203,17 @@ class PlusDirectionalIndicator(_DirectionalIndicator): - +dm = upmove if upmove > downmove and upmove > 0 else 0 - +di = 100 * MovingAverage(+dm, period) / atr(period) - The moving average used is the one originally defined by Wilder, - the SmoothedMovingAverage + 默认 Moving Average 使用 Wilder 原始定义中的 SmoothedMovingAverage。 See: - https://en.wikipedia.org/wiki/Average_directional_movement_index + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(PlusDirectionalIndicator) ''' alias = (('PlusDI', '+DI'),) lines = ('plusDI',) @@ -187,18 +228,18 @@ def __init__(self): class MinusDirectionalIndicator(_DirectionalIndicator): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"*. + J. Welles Wilder, Jr. 于 1978 年定义的 -DI indicator。 + + 用于衡量趋势强度。 + + 该 indicator 显示 -DI。 - Intended to measure trend strength + Args: + period: 计算周期。 + movav: 用于平滑的 Moving Average 类型。 - This indicator shows -DI: - - Use PlusDirectionalIndicator (PlusDI) to get +DI - - Use Directional Indicator (DI) to get +DI, -DI - - Use AverageDirectionalIndex (ADX) to get ADX - - Use AverageDirectionalIndexRating (ADXR) to get ADX, ADXR - - Use DirectionalMovementIndex (DMI) to get ADX, +DI, -DI - - Use DirectionalMovement (DM) to get ADX, ADXR, +DI, -DI + Returns: + MinusDirectionalIndicator: 输出 ``minusDI`` line 的 indicator。 Formula: - upmove = high - high(-1) @@ -206,11 +247,17 @@ class MinusDirectionalIndicator(_DirectionalIndicator): - -dm = downmove if downmove > upmove and downmove > 0 else 0 - -di = 100 * MovingAverage(-dm, period) / atr(period) - The moving average used is the one originally defined by Wilder, - the SmoothedMovingAverage + 默认 Moving Average 使用 Wilder 原始定义中的 SmoothedMovingAverage。 See: - https://en.wikipedia.org/wiki/Average_directional_movement_index + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(MinusDirectionalIndicator) ''' alias = (('MinusDI', '-DI'),) lines = ('minusDI',) @@ -225,18 +272,18 @@ def __init__(self): class AverageDirectionalMovementIndex(_DirectionalIndicator): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"*. + J. Welles Wilder, Jr. 于 1978 年定义的 Average Directional Movement Index。 + + 用于衡量趋势强度。 - Intended to measure trend strength + 该 indicator 只显示 ADX。 - This indicator only shows ADX: - - Use PlusDirectionalIndicator (PlusDI) to get +DI - - Use MinusDirectionalIndicator (MinusDI) to get -DI - - Use Directional Indicator (DI) to get +DI, -DI - - Use AverageDirectionalIndexRating (ADXR) to get ADX, ADXR - - Use DirectionalMovementIndex (DMI) to get ADX, +DI, -DI - - Use DirectionalMovement (DM) to get ADX, ADXR, +DI, -DI + Args: + period: 计算周期。 + movav: 用于平滑的 Moving Average 类型。 + + Returns: + AverageDirectionalMovementIndex: 输出 ``adx`` line 的 indicator。 Formula: - upmove = high - high(-1) @@ -248,11 +295,17 @@ class AverageDirectionalMovementIndex(_DirectionalIndicator): - dx = 100 * abs(+di - -di) / (+di + -di) - adx = MovingAverage(dx, period) - The moving average used is the one originally defined by Wilder, - the SmoothedMovingAverage + 默认 Moving Average 使用 Wilder 原始定义中的 SmoothedMovingAverage。 See: - https://en.wikipedia.org/wiki/Average_directional_movement_index + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(AverageDirectionalMovementIndex) ''' alias = ('ADX',) @@ -269,20 +322,21 @@ def __init__(self): class AverageDirectionalMovementIndexRating(AverageDirectionalMovementIndex): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"*. + J. Welles Wilder, Jr. 于 1978 年定义的 ADXR。 + + 用于衡量趋势强度。 - Intended to measure trend strength. + ADXR 是当前 ADX 与 ``period`` 个 bar 之前 ADX 的平均值。 - ADXR is the average of ADX with a value period bars ago + 该 indicator 显示 ADX 与 ADXR。 - This indicator shows the ADX and ADXR: - - Use PlusDirectionalIndicator (PlusDI) to get +DI - - Use MinusDirectionalIndicator (MinusDI) to get -DI - - Use Directional Indicator (DI) to get +DI, -DI - - Use AverageDirectionalIndex (ADX) to get ADX - - Use DirectionalMovementIndex (DMI) to get ADX, +DI, -DI - - Use DirectionalMovement (DM) to get ADX, ADXR, +DI, -DI + Args: + period: 计算周期。 + movav: 用于平滑的 Moving Average 类型。 + + Returns: + AverageDirectionalMovementIndexRating: 输出 ``adx`` 与 ``adxr`` line + 的 indicator。 Formula: - upmove = high - high(-1) @@ -295,11 +349,17 @@ class AverageDirectionalMovementIndexRating(AverageDirectionalMovementIndex): - adx = MovingAverage(dx, period) - adxr = (adx + adx(-period)) / 2 - The moving average used is the one originally defined by Wilder, - the SmoothedMovingAverage + 默认 Moving Average 使用 Wilder 原始定义中的 SmoothedMovingAverage。 See: - https://en.wikipedia.org/wiki/Average_directional_movement_index + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(AverageDirectionalMovementIndexRating) ''' alias = ('ADXR',) @@ -315,18 +375,19 @@ def __init__(self): class DirectionalMovementIndex(AverageDirectionalMovementIndex, DirectionalIndicator): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"*. + J. Welles Wilder, Jr. 于 1978 年定义的 Directional Movement Index。 + + 用于衡量趋势强度。 - Intended to measure trend strength + 该 indicator 显示 ADX、+DI 与 -DI。 - This indicator shows the ADX, +DI, -DI: - - Use PlusDirectionalIndicator (PlusDI) to get +DI - - Use MinusDirectionalIndicator (MinusDI) to get -DI - - Use Directional Indicator (DI) to get +DI, -DI - - Use AverageDirectionalIndex (ADX) to get ADX - - Use AverageDirectionalIndexRating (ADXRating) to get ADX, ADXR - - Use DirectionalMovement (DM) to get ADX, ADXR, +DI, -DI + Args: + period: 计算周期。 + movav: 用于平滑的 Moving Average 类型。 + + Returns: + DirectionalMovementIndex: 输出 ``adx``、``plusDI`` 与 ``minusDI`` line + 的 indicator。 Formula: - upmove = high - high(-1) @@ -338,11 +399,17 @@ class DirectionalMovementIndex(AverageDirectionalMovementIndex, - dx = 100 * abs(+di - -di) / (+di + -di) - adx = MovingAverage(dx, period) - The moving average used is the one originally defined by Wilder, - the SmoothedMovingAverage + 默认 Moving Average 使用 Wilder 原始定义中的 SmoothedMovingAverage。 See: - https://en.wikipedia.org/wiki/Average_directional_movement_index + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(DirectionalMovementIndex) ''' alias = ('DMI',) @@ -350,19 +417,19 @@ class DirectionalMovementIndex(AverageDirectionalMovementIndex, class DirectionalMovement(AverageDirectionalMovementIndexRating, DirectionalIndicator): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"*. + J. Welles Wilder, Jr. 于 1978 年定义的完整 Directional Movement indicator。 + + 用于衡量趋势强度。 - Intended to measure trend strength + 该 indicator 显示 ADX、ADXR、+DI 与 -DI。 - This indicator shows ADX, ADXR, +DI, -DI. + Args: + period: 计算周期。 + movav: 用于平滑的 Moving Average 类型。 - - Use PlusDirectionalIndicator (PlusDI) to get +DI - - Use MinusDirectionalIndicator (MinusDI) to get -DI - - Use Directional Indicator (DI) to get +DI, -DI - - Use AverageDirectionalIndex (ADX) to get ADX - - Use AverageDirectionalIndexRating (ADXR) to get ADX, ADXR - - Use DirectionalMovementIndex (DMI) to get ADX, +DI, -DI + Returns: + DirectionalMovement: 输出 ``adx``、``adxr``、``plusDI`` 与 ``minusDI`` + line 的 indicator。 Formula: - upmove = high - high(-1) @@ -374,10 +441,16 @@ class DirectionalMovement(AverageDirectionalMovementIndexRating, - dx = 100 * abs(+di - -di) / (+di + -di) - adx = MovingAverage(dx, period) - The moving average used is the one originally defined by Wilder, - the SmoothedMovingAverage + 默认 Moving Average 使用 Wilder 原始定义中的 SmoothedMovingAverage。 See: - https://en.wikipedia.org/wiki/Average_directional_movement_index + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(DirectionalMovement) ''' alias = ('DM',) diff --git a/backtrader/indicators/dma.py b/backtrader/indicators/dma.py index 596e17023..df141ec82 100644 --- a/backtrader/indicators/dma.py +++ b/backtrader/indicators/dma.py @@ -26,11 +26,21 @@ class DicksonMovingAverage(MovingAverageBase): - '''By Nathan Dickson + '''Nathan Dickson 提出的 Dickson Moving Average。 - The *Dickson Moving Average* combines the ``ZeroLagIndicator`` (aka - *ErrorCorrecting* or *EC*) by *Ehlers*, and the ``HullMovingAverage`` to - try to deliver a result close to that of the *Jurik* Moving Averages + *Dickson Moving Average* 结合 Ehlers 的 ``ZeroLagIndicator``(也称 + *ErrorCorrecting* 或 *EC*)与 ``HullMovingAverage``,尝试得到接近 + *Jurik* Moving Averages 的效果。 + + Args: + period: ZeroLagIndicator 的计算周期。 + gainlimit: ZeroLagIndicator 的增益上限。 + hperiod: HullMovingAverage 的计算周期。 + _movav: ZeroLagIndicator 使用的 Moving Average 类型。 + _hma: 第二个 Moving Average 类型,默认使用 HullMovingAverage。 + + Returns: + DicksonMovingAverage: 输出 ``dma`` line 的 Moving Average indicator。 Formula: - ec = ZeroLagIndicator(period, gainlimit) @@ -38,18 +48,22 @@ class DicksonMovingAverage(MovingAverageBase): - dma = (ec + hma) / 2 - - The default moving average for the *ZeroLagIndicator* is EMA, but can - be changed with the parameter ``_movav`` + - *ZeroLagIndicator* 默认使用 EMA,可通过参数 ``_movav`` 修改。 - .. note:: the passed moving average must calculate alpha (and 1 - - alpha) and make them available as attributes ``alpha`` and - ``alpha1`` + .. note:: 传入的 Moving Average 必须计算 alpha(以及 1 - alpha),并以 + ``alpha`` 与 ``alpha1`` 属性暴露。 - - The 2nd moving averag can be changed from *Hull* to anything else with - the param *_hma* + - 第 2 个 Moving Average 可通过参数 ``_hma`` 从 *Hull* 改为其他类型。 See also: - https://www.reddit.com/r/algotrading/comments/4xj3vh/dickson_moving_average + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(DicksonMovingAverage, period=30) ''' alias = ('DMA', 'DicksonMA',) lines = ('dma',) @@ -75,5 +89,5 @@ def __init__(self): self.lines.dma = (ec + hull) / 2.0 - # To make mixins work - super at the end for cooperative inheritance + # 为了让 mixin 生效,将 super 放在末尾以支持协作式继承 super(DicksonMovingAverage, self).__init__() diff --git a/backtrader/indicators/dpo.py b/backtrader/indicators/dpo.py index f150ace5a..430994b3a 100644 --- a/backtrader/indicators/dpo.py +++ b/backtrader/indicators/dpo.py @@ -18,7 +18,7 @@ # along with this program. If not, see . # ############################################################################### -# Python 2/3 compatibility imports +# Python 2/3 兼容导入 from __future__ import (absolute_import, division, print_function, unicode_literals) @@ -27,10 +27,16 @@ class DetrendedPriceOscillator(Indicator): ''' - Defined by Joe DiNapoli in his book *"Trading with DiNapoli levels"* + Joe DiNapoli 在 *"Trading with DiNapoli levels"* 中定义的 DPO 指标。 - It measures the price variations against a Moving Average (the trend) - and therefore removes the "trend" factor from the price. + 它衡量价格相对 Moving Average(趋势)的变化,从而从价格中移除“趋势”因素。 + + Args: + period: Moving Average 周期。 + movav: 使用的 Moving Average 类型。 + + Returns: + DetrendedPriceOscillator: 输出 ``dpo`` line 的 indicator。 Formula: - movav = MovingAverage(close, period) @@ -38,31 +44,37 @@ class DetrendedPriceOscillator(Indicator): See: - http://en.wikipedia.org/wiki/Detrended_price_oscillator + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(DetrendedPriceOscillator, period=20) ''' - # Named alias for invocation + # 用于调用的命名别名 alias = ('DPO',) - # Named output lines + # 命名输出 line lines = ('dpo',) - # Accepted parameters (and defaults) - - # MovAvg also parameter to allow experimentation + # 可接受参数及默认值;movav 也作为参数,便于实验 params = (('period', 20), ('movav', MovAv.Simple)) - # Emphasize central 0.0 line in plot + # 绘图时强调中心 0.0 线 plotinfo = dict(plothlines=[0.0]) - # Indicator information after the name (in brackets) + # indicator 名称后的信息(括号中) def _plotlabel(self): plabels = [self.p.period] plabels += [self.p.movav] * self.p.notdefault('movav') return plabels def __init__(self): - # Create the Moving Average + # 创建 Moving Average ma = self.p.movav(self.data, period=self.p.period) - # Calculate value (look back period/2 + 1 in MA) and bind to 'dpo' line + # 计算值(在 MA 中回看 period/2 + 1),并绑定到 dpo line self.lines.dpo = self.data - ma(-self.p.period // 2 + 1) super(DetrendedPriceOscillator, self).__init__() diff --git a/backtrader/indicators/dv2.py b/backtrader/indicators/dv2.py index 0bde533ab..55f910989 100644 --- a/backtrader/indicators/dv2.py +++ b/backtrader/indicators/dv2.py @@ -30,15 +30,28 @@ class DV2(Indicator): ''' - RSI(2) alternative - Developed by David Varadi of http://cssanalytics.wordpress.com/ + RSI(2) 的替代指标,由 David Varadi 提出。 - This seems to be the *Bounded* version. + Args: + period: PercentRank 的统计周期。 + maperiod: 内部 moving average 周期。 + _movav: 内部使用的 Moving Average 类型。 + + Returns: + DV2: 输出 ``dv2`` line 的 indicator。 + + 该实现看起来是 *Bounded* 版本。 See also: - http://web.archive.org/web/20131216100741/http://quantingdutchman.wordpress.com/2010/08/06/dv2-indicator-for-amibroker/ + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(DV2) ''' params = ( ('period', 252), diff --git a/backtrader/indicators/ema.py b/backtrader/indicators/ema.py index a3054c24d..9560e51dd 100644 --- a/backtrader/indicators/ema.py +++ b/backtrader/indicators/ema.py @@ -26,9 +26,15 @@ class ExponentialMovingAverage(MovingAverageBase): ''' - A Moving Average that smoothes data exponentially over time. + 随时间对数据做指数平滑的 Moving Average。 - It is a subclass of SmoothingMovingAverage. + Args: + period: 平滑周期。 + + Returns: + ExponentialMovingAverage: 输出 ``ema`` line 的 indicator。 + + 它是 ``SmoothingMovingAverage`` 的子类。 - self.smfactor -> 2 / (1 + period) - self.smfactor1 -> `1 - self.smfactor` @@ -38,13 +44,19 @@ class ExponentialMovingAverage(MovingAverageBase): See also: - http://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(ExponentialMovingAverage, period=20) ''' alias = ('EMA', 'MovingAverageExponential',) lines = ('ema',) def __init__(self): - # Before super to ensure mixins (right-hand side in subclassing) - # can see the assignment operation and operate on the line + # 放在 super 之前,确保 mixin(子类化时右侧基类)能看到赋值操作并处理该 line self.lines[0] = es = ExponentialSmoothing( self.data, period=self.p.period, diff --git a/backtrader/indicators/envelope.py b/backtrader/indicators/envelope.py index ba117613b..380ce58ff 100644 --- a/backtrader/indicators/envelope.py +++ b/backtrader/indicators/envelope.py @@ -28,11 +28,10 @@ class EnvelopeMixIn(object): ''' - MixIn class to create a subclass with another indicator. The main line of - that indicator will be surrounded by an upper and lower band separated a - given "perc"entage from the input main line + Envelope 的 MixIn 基类,用于与另一个 indicator 组合,为其主 line 创建上下轨。 + 上下轨与输入主 line 相差给定百分比 ``perc``。 - The usage is: + 用法: - Class XXXEnvelope(XXX, EnvelopeMixIn) @@ -49,7 +48,7 @@ class EnvelopeMixIn(object): plotlines = dict(top=dict(_samecolor=True), bot=dict(_samecolor=True),) def __init__(self): - # Mix-in & directly from object -> does not necessarily need super + # Mix-in 直接来自 object,并不一定需要 super # super(EnvelopeMixIn, self).__init__() perc = self.p.perc / 100.0 @@ -60,12 +59,13 @@ def __init__(self): class _EnvelopeBase(Indicator): + '''Envelope 的基类,用于保留源 data 并与上下轨一起绘图。''' lines = ('src',) - # plot the envelope lines along the passed source + # 将 envelope line 与传入源一起绘制 plotinfo = dict(subplot=False) - # Do not replot the data line + # 不重复绘制 data line plotlines = dict(src=dict(_plotskip=True)) def __init__(self): @@ -75,8 +75,13 @@ def __init__(self): class Envelope(_EnvelopeBase, EnvelopeMixIn): ''' - It creates envelopes bands separated from the source data by a given - percentage + 按给定百分比在源 data 上下创建 envelope band。 + + Args: + perc: 上下轨相对源 data 的百分比距离。 + + Returns: + Envelope: 输出 ``src``、``top`` 与 ``bot`` line 的 indicator。 Formula: - src = datasource @@ -85,14 +90,21 @@ class Envelope(_EnvelopeBase, EnvelopeMixIn): See also: - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:moving_average_envelopes + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(Envelope, perc=2.5) ''' -# Automatic creation of Moving Average Envelope classes +# 自动创建 Moving Average Envelope 类 for movav in MovingAverage._movavs[1:]: _newclsdoc = ''' - %s and envelope bands separated "perc" from it + %s 及其按 ``perc`` 分离的 envelope band。 Formula: - %s (from %s) @@ -102,7 +114,7 @@ class Envelope(_EnvelopeBase, EnvelopeMixIn): See also: - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:moving_average_envelopes ''' - # Skip aliases - they will be created automatically + # 跳过 alias,它们会自动创建 if getattr(movav, 'aliased', ''): continue diff --git a/backtrader/indicators/hadelta.py b/backtrader/indicators/hadelta.py index d2b0a7231..b2c463123 100644 --- a/backtrader/indicators/hadelta.py +++ b/backtrader/indicators/hadelta.py @@ -30,21 +30,33 @@ class haDelta(bt.Indicator): - '''Heikin Ashi Delta. Defined by Dan Valcu in his book "Heikin-Ashi: How to - Trade Without Candlestick Patterns ". + '''Dan Valcu 在 *"Heikin-Ashi: How to Trade Without Candlestick Patterns"* + 中定义的 Heikin Ashi Delta。 - This indicator measures difference between Heikin Ashi close and open of - Heikin Ashi candles, the body of the candle. + 该 indicator 衡量 Heikin Ashi candle 的 close 与 open 之差,也就是 candle body。 - To get signals add haDelta smoothed by 3 period moving average. + 信号通常来自 3 周期 Moving Average 平滑后的 haDelta。 - For correct use, the data for the indicator must have been previously - passed by the Heikin Ahsi filter. + 若 ``autoheikin`` 为 False,传入数据应已经通过 Heikin Ashi filter 处理。 + + Args: + period: 平滑 haDelta 的 Moving Average 周期。 + movav: 用于平滑的 Moving Average 类型。 + autoheikin: 是否自动先计算 HeikinAshi 数据。 + + Returns: + haDelta: 输出 ``haDelta`` 与 ``smoothed`` line 的 indicator。 Formula: - haDelta = Heikin Ashi close - Heikin Ashi open - smoothed = movav(haDelta, period) + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(haDelta, period=3) ''' alias = ('haD',) diff --git a/backtrader/indicators/heikinashi.py b/backtrader/indicators/heikinashi.py index a58ef2751..a220527f7 100644 --- a/backtrader/indicators/heikinashi.py +++ b/backtrader/indicators/heikinashi.py @@ -31,7 +31,14 @@ class HeikinAshi(bt.Indicator): ''' - Heikin Ashi candlesticks in the forms of lines + 以 line 形式输出 Heikin Ashi candlestick 数据。 + + Args: + data: 含有 open/high/low/close line 的数据源。 + + Returns: + HeikinAshi: 输出 ``ha_open``、``ha_high``、``ha_low`` 与 ``ha_close`` + line 的 indicator。 Formula: ha_open = (ha_open(-1) + ha_close(-1)) / 2 @@ -42,6 +49,13 @@ class HeikinAshi(bt.Indicator): See also: https://en.wikipedia.org/wiki/Candlestick_chart#Heikin_Ashi_candlesticks http://stockcharts.com/school/doku.php?id=chart_school:chart_analysis:heikin_ashi + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(HeikinAshi) ''' lines = ('ha_open', 'ha_high', 'ha_low', 'ha_close',) @@ -70,5 +84,5 @@ def __init__(self): super(HeikinAshi, self).__init__() def prenext(self): - # seed recursive value + # 为递归值设置初始种子 self.lines.ha_open[0] = (self.data.open[0] + self.data.close[0]) / 2.0 diff --git a/backtrader/indicators/hma.py b/backtrader/indicators/hma.py index 5f9185277..5c331b5a2 100644 --- a/backtrader/indicators/hma.py +++ b/backtrader/indicators/hma.py @@ -25,14 +25,19 @@ from . import MovingAverageBase, MovAv -# Inherits from MovingAverageBase to auto-register as MovingAverage type +# 继承 MovingAverageBase,以自动注册为 MovingAverage 类型 class HullMovingAverage(MovingAverageBase): - '''By Alan Hull + '''Alan Hull 提出的 Hull Moving Average。 - The Hull Moving Average solves the age old dilemma of making a moving - average more responsive to current price activity whilst maintaining curve - smoothness. In fact the HMA almost eliminates lag altogether and manages to - improve smoothing at the same time. + HMA 尝试解决 Moving Average 既要更快响应当前价格活动、又要保持曲线平滑的难题。 + 它几乎消除了 lag,同时提升了平滑效果。 + + Args: + period: 计算周期。 + _movav: 内部使用的 Moving Average 类型。 + + Returns: + HullMovingAverage: 输出 ``hma`` line 的 indicator。 Formula: - hma = wma(2 * wma(data, period // 2) - wma(data, period), sqrt(period)) @@ -42,17 +47,23 @@ class HullMovingAverage(MovingAverageBase): Note: - - Please note that the final minimum period is not the period passed with - the parameter ``period``. A final moving average on moving average is - done in which the period is the *square root* of the original. + - 最终 minimum period 并不是参数 ``period`` 本身。最后还会对 moving average + 再做一次 moving average,其周期是原始周期的 *square root*。 + + 默认 ``30`` 的情况下,在 moving average 产出非 NAN 值前,最终 minimum + period 为 ``34``。 + + --- + 交互界面使用示范: - In the default case of ``30`` the final minimum period before the - moving average produces a non-NAN value is ``34`` + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(HullMovingAverage, period=30) ''' alias = ('HMA', 'HullMA',) lines = ('hma',) - # param 'period' is inherited from MovingAverageBase + # period 参数继承自 MovingAverageBase params = (('_movav', MovAv.WMA),) def __init__(self): @@ -62,5 +73,5 @@ def __init__(self): sqrtperiod = pow(self.params.period, 0.5) self.lines.hma = self.p._movav(wma2 - wma, period=int(sqrtperiod)) - # Done after calc to ensure coop inheritance and composition work + # 计算后再调用,确保协作继承和组合可用 super(HullMovingAverage, self).__init__() diff --git a/backtrader/indicators/hurst.py b/backtrader/indicators/hurst.py index 90c57b1b8..d5d03d8f1 100644 --- a/backtrader/indicators/hurst.py +++ b/backtrader/indicators/hurst.py @@ -29,32 +29,45 @@ class HurstExponent(PeriodN): ''' + Hurst Exponent 指标,用于判断序列更接近随机游走、均值回归还是趋势状态。 + + Args: + period: 计算窗口周期。 + lag_start: lag 起始值;为空时使用 ``2``。 + lag_end: lag 结束值;为空时使用 ``self.p.period / 2``。 + + Returns: + HurstExponent: 输出 ``hurst`` line 的 indicator。 + References: - https://www.quantopian.com/posts/hurst-exponent - https://www.quantopian.com/posts/some-code-from-ernie-chans-new-book-implemented-in-python - Interpretation of the results + 结果解释: 1. Geometric random walk (H=0.5) 2. Mean-reverting series (H<0.5) - 3. Trending Series (H>0.5) + 3. Trending series (H>0.5) + + 重要说明: - Important notes: + - 默认 period 为 ``40``,但用户实验表明至少使用 2000 个样本(即 period 至少 + 2000)更容易得到稳定值。 - - The default period is ``40``, but experimentation by users has shown - that it would be advisable to have at least 2000 samples (i.e.: a - period of at least 2000) to have stable values. + - 未指定参数时,``lag_start`` 和 ``lag_end`` 默认分别为 ``2`` 和 + ``self.p.period / 2``。 - - The `lag_start` and `lag_end` values will default to be ``2`` and - ``self.p.period / 2`` unless the parameters are specified. + 用户实验也表明,约 ``10`` 和 ``500`` 的取值表现较好。 - Experimentation by users has also shown that values of around ``10`` - and ``500`` produce good results + 原始取值 ``(40, 2, self.p.period / 2)`` 为保持向后兼容而保留。 - The original values (40, 2, self.p.period / 2) are kept for backwards - compatibility + --- + 交互界面使用示范: + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(HurstExponent, period=2000) ''' frompackages = ( ('numpy', ('asarray', 'log10', 'polyfit', 'sqrt', 'std', 'subtract')), @@ -63,9 +76,9 @@ class HurstExponent(PeriodN): alias = ('Hurst',) lines = ('hurst',) params = ( - ('period', 40), # 2000 was proposed - ('lag_start', None), # 10 was proposed - ('lag_end', None), # 500 was proposed + ('period', 40), # 曾建议使用 2000 + ('lag_start', None), # 曾建议使用 10 + ('lag_end', None), # 曾建议使用 500 ) def _plotlabel(self): @@ -76,21 +89,21 @@ def _plotlabel(self): def __init__(self): super(HurstExponent, self).__init__() - # Prepare the lags array + # 准备 lags 数组 self._lag_start = lag_start = self.p.lag_start or 2 self._lag_end = lag_end = self.p.lag_end or (self.p.period // 2) self.lags = asarray(range(lag_start, lag_end)) self.log10lags = log10(self.lags) def next(self): - # Fetch the data + # 获取数据 ts = asarray(self.data.get(size=self.p.period)) - # Calculate the array of the variances of the lagged differences + # 计算 lagged differences 的方差数组 tau = [sqrt(std(subtract(ts[lag:], ts[:-lag]))) for lag in self.lags] - # Use a linear fit to estimate the Hurst Exponent + # 使用线性拟合估算 Hurst Exponent poly = polyfit(self.log10lags, log10(tau), 1) - # Return the Hurst exponent from the polyfit output + # 从 polyfit 输出中返回 Hurst exponent self.lines.hurst[0] = poly[0] * 2.0 diff --git a/backtrader/indicators/ichimoku.py b/backtrader/indicators/ichimoku.py index c08543212..1cb4ed8bc 100644 --- a/backtrader/indicators/ichimoku.py +++ b/backtrader/indicators/ichimoku.py @@ -27,26 +27,43 @@ class Ichimoku(bt.Indicator): ''' - Developed and published in his book in 1969 by journalist Goichi Hosoda + 记者 Goichi Hosoda 开发,并于 1969 年在其书中发表的一目均衡表。 + + Args: + tenkan: tenkan_sen 的计算周期。 + kijun: kijun_sen 的计算周期。 + senkou: senkou_span_b 的计算周期。 + senkou_lead: senkou span 向未来平移的 bar 数。 + chikou: chikou span 向过去平移的 bar 数。 + + Returns: + Ichimoku: 输出 ``tenkan_sen``、``kijun_sen``、``senkou_span_a``、 + ``senkou_span_b`` 与 ``chikou_span`` line 的 indicator。 Formula: - tenkan_sen = (Highest(High, tenkan) + Lowest(Low, tenkan)) / 2.0 - kijun_sen = (Highest(High, kijun) + Lowest(Low, kijun)) / 2.0 - The next 2 are pushed 26 bars into the future + 以下 2 个 line 会向未来推进 26 个 bar: - senkou_span_a = (tenkan_sen + kijun_sen) / 2.0 - senkou_span_b = ((Highest(High, senkou) + Lowest(Low, senkou)) / 2.0 - This is pushed 26 bars into the past + 以下 line 会向过去推进 26 个 bar: - chikou = close - The cloud (Kumo) is formed by the area between the senkou_spans + 云图(Kumo)由两个 senkou span 之间的区域形成。 See: - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:ichimoku_cloud + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(Ichimoku) ''' lines = ('tenkan_sen', 'kijun_sen', 'senkou_span_a', 'senkou_span_b', 'chikou_span',) @@ -54,8 +71,8 @@ class Ichimoku(bt.Indicator): ('tenkan', 9), ('kijun', 26), ('senkou', 52), - ('senkou_lead', 26), # forward push - ('chikou', 26), # backwards push + ('senkou_lead', 26), # 向未来推进 + ('chikou', 26), # 向过去推进 ) plotinfo = dict(subplot=False) diff --git a/backtrader/indicators/kama.py b/backtrader/indicators/kama.py index 704d6b40f..52a9f3133 100644 --- a/backtrader/indicators/kama.py +++ b/backtrader/indicators/kama.py @@ -26,19 +26,24 @@ class AdaptiveMovingAverage(MovingAverageBase): ''' - Defined by Perry Kaufman in his book `"Smarter Trading"`. + Perry Kaufman 在 *"Smarter Trading"* 中定义的 Adaptive Moving Average。 - It is A Moving Average with a continuously scaled smoothing factor by - taking into account market direction and volatility. The smoothing factor - is calculated from 2 ExponetialMovingAverage smoothing factors, a fast one - and slow one. + 该 Moving Average 会结合市场方向与波动性,持续缩放 smoothing factor。 + smoothing factor 来自两个 ExponentialMovingAverage 平滑因子:一个快周期, + 一个慢周期。 - If the market trends the value will tend to the fast ema smoothing - period. If the market doesn't trend it will move towards the slow EMA - smoothing period. + 当市场呈趋势状态时,数值会更接近快速 EMA 平滑周期;当市场缺乏趋势时, + 它会向慢速 EMA 平滑周期移动。 - It is a subclass of SmoothingMovingAverage, overriding once to account for - the live nature of the smoothing factor + 它是 SmoothingMovingAverage 的子类,通过动态 smoothing factor 处理实时变化。 + + Args: + period: 计算方向与波动性的周期。 + fast: 快速 EMA 平滑周期。 + slow: 慢速 EMA 平滑周期。 + + Returns: + AdaptiveMovingAverage: 输出 ``kama`` line 的 Moving Average indicator。 Formula: - direction = close - close_period @@ -50,29 +55,35 @@ class AdaptiveMovingAverage(MovingAverageBase): - smfactor = squared(efficienty_ratio * (fast - slow) + slow) - smfactor1 = 1.0 - smfactor - - The initial seed value is a SimpleMovingAverage + - 初始种子值为 SimpleMovingAverage See also: - http://fxcodebase.com/wiki/index.php/Kaufman's_Adaptive_Moving_Average_(KAMA) - http://www.metatrader5.com/en/terminal/help/analytics/indicators/trend_indicators/ama - http://help.cqg.com/cqgic/default.htm#!Documents/adaptivemovingaverag2.htm + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(AdaptiveMovingAverage, period=30) ''' alias = ('KAMA', 'MovingAverageAdaptive',) lines = ('kama',) params = (('fast', 2), ('slow', 30)) def __init__(self): - # Before super to ensure mixins (right-hand side in subclassing) - # can see the assignment operation and operate on the line + # 放在 super 之前,确保 mixin(子类化时右侧基类)能看到赋值并处理该 line direction = self.data - self.data(-self.p.period) volatility = SumN(abs(self.data - self.data(-1)), period=self.p.period) er = abs(direction / volatility) # efficiency ratio - fast = 2.0 / (self.p.fast + 1.0) # fast ema smoothing factor - slow = 2.0 / (self.p.slow + 1.0) # slow ema smoothing factor + fast = 2.0 / (self.p.fast + 1.0) # fast EMA smoothing factor + slow = 2.0 / (self.p.slow + 1.0) # slow EMA smoothing factor - sc = pow((er * (fast - slow)) + slow, 2) # scalable constant + sc = pow((er * (fast - slow)) + slow, 2) # 可缩放常量 self.lines[0] = ExponentialSmoothingDynamic(self.data, period=self.p.period, diff --git a/backtrader/indicators/kst.py b/backtrader/indicators/kst.py index 5933eda42..af3fb5c49 100644 --- a/backtrader/indicators/kst.py +++ b/backtrader/indicators/kst.py @@ -27,8 +27,25 @@ class KnowSureThing(bt.Indicator): ''' - It is a "summed" momentum indicator. Developed by Martin Pring and - published in 1992 in Stocks & Commodities. + Martin Pring 开发并于 1992 年在 *Stocks & Commodities* 发表的 + "summed" momentum indicator。 + + Args: + rp1: 第 1 个 ROC100 周期。 + rp2: 第 2 个 ROC100 周期。 + rp3: 第 3 个 ROC100 周期。 + rp4: 第 4 个 ROC100 周期。 + rma1: 第 1 个 ROC 平滑周期。 + rma2: 第 2 个 ROC 平滑周期。 + rma3: 第 3 个 ROC 平滑周期。 + rma4: 第 4 个 ROC 平滑周期。 + rsignal: signal line 的平滑周期。 + rfactors: 应用于各个 MovAv(ROC) 的权重列表。 + _rmovav: 用于 ROC 平滑的 Moving Average 类型。 + _smovav: 用于 signal line 的 Moving Average 类型。 + + Returns: + KnowSureThing: 输出 ``kst`` 与 ``signal`` line 的 indicator。 Formula: - rcma1 = MovAv(roc100(rp1), period) @@ -42,15 +59,12 @@ class KnowSureThing(bt.Indicator): See: - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:know_sure_thing_kst - Params - - - ``rma1``, ``rma2``, ``rma3``, ``rma4``: for the MovingAverages on ROCs - - ``rp1``, ``rp2``, ``rp3``, ``rp4``: for the ROCs - - ``rsig``: for the MovingAverage for the signal line - - ``rfactors``: list of factors to apply to the different MovAv(ROCs) - - ``_movav`` and ``_movavs``, allows to change the Moving Average type - applied for the calculation of kst and signal + --- + 交互界面使用示范: + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(KnowSureThing) ''' alias = ('KST',) lines = ('kst', 'signal',) diff --git a/backtrader/indicators/lrsi.py b/backtrader/indicators/lrsi.py index d6a9817e0..01cba8de3 100644 --- a/backtrader/indicators/lrsi.py +++ b/backtrader/indicators/lrsi.py @@ -29,15 +29,27 @@ class LaguerreRSI(PeriodN): ''' - Defined by John F. Ehlers in `Cybernetic Analysis for Stock and Futures`, - 2004, published by Wiley. `ISBN: 978-0-471-46307-8` + John F. Ehlers 在 Wiley 2004 年出版的 *Cybernetic Analysis for Stock and + Futures* 中定义。`ISBN: 978-0-471-46307-8` - The Laguerre RSI tries to implements a better RSI by providing a sort of - *Time Warp without Time Travel* using a Laguerre filter. This provides for - faster reactions to price changes + Laguerre RSI 通过 Laguerre filter 提供一种 *Time Warp without Time Travel* + 的效果,尝试实现反应更快的 RSI。 - ``gamma`` is meant to have values between ``0.2`` and ``0.8``, with the - best balance found theoretically at the default of ``0.5`` + ``gamma`` 通常取 ``0.2`` 到 ``0.8`` 之间,理论上默认值 ``0.5`` 有较好平衡。 + + Args: + gamma: Laguerre filter 的平滑参数。 + period: 最小计算周期。 + + Returns: + LaguerreRSI: 输出 ``lrsi`` line 的 indicator。 + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(LaguerreRSI, gamma=0.5) ''' alias = ('LRSI',) lines = ('lrsi',) @@ -54,11 +66,11 @@ class LaguerreRSI(PeriodN): l0, l1, l2, l3 = 0.0, 0.0, 0.0, 0.0 def next(self): - l0_1 = self.l0 # cache previous intermediate values + l0_1 = self.l0 # 缓存上一轮中间值 l1_1 = self.l1 l2_1 = self.l2 - g = self.p.gamma # avoid more lookups + g = self.p.gamma # 避免重复查找 self.l0 = l0 = (1.0 - g) * self.data + g * l0_1 self.l1 = l1 = -g * l0 + l0_1 + g * l1_1 self.l2 = l2 = -g * l1 + l1_1 + g * l2_1 @@ -87,11 +99,23 @@ def next(self): class LaguerreFilter(PeriodN): ''' - Defined by John F. Ehlers in `Cybernetic Analysis for Stock and Futures`, - 2004, published by Wiley. `ISBN: 978-0-471-46307-8` + John F. Ehlers 在 Wiley 2004 年出版的 *Cybernetic Analysis for Stock and + Futures* 中定义。`ISBN: 978-0-471-46307-8` + + ``gamma`` 通常取 ``0.2`` 到 ``0.8`` 之间,理论上默认值 ``0.5`` 有较好平衡。 + + Args: + gamma: Laguerre filter 的平滑参数。 + + Returns: + LaguerreFilter: 输出 ``lfilter`` line 的 indicator。 + + --- + 交互界面使用示范: - ``gamma`` is meant to have values between ``0.2`` and ``0.8``, with the - best balance found theoretically at the default of ``0.5`` + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(LaguerreFilter, gamma=0.5) ''' alias = ('LAGF',) lines = ('lfilter',) @@ -101,11 +125,11 @@ class LaguerreFilter(PeriodN): l0, l1, l2, l3 = 0.0, 0.0, 0.0, 0.0 def next(self): - l0_1 = self.l0 # cache previous intermediate values + l0_1 = self.l0 # 缓存上一轮中间值 l1_1 = self.l1 l2_1 = self.l2 - g = self.p.gamma # avoid more lookups + g = self.p.gamma # 避免重复查找 self.l0 = l0 = (1.0 - g) * self.data + g * l0_1 self.l1 = l1 = -g * l0 + l0_1 + g * l1_1 self.l2 = l2 = -g * l1 + l1_1 + g * l2_1 diff --git a/backtrader/indicators/mabase.py b/backtrader/indicators/mabase.py index c9dc8b437..797f77f1d 100644 --- a/backtrader/indicators/mabase.py +++ b/backtrader/indicators/mabase.py @@ -27,19 +27,17 @@ class MovingAverage(object): - '''MovingAverage (alias MovAv) + '''MovingAverage(别名 MovAv)的占位基类,用于集中登记所有 Moving Average 类型。 - A placeholder to gather all Moving Average Types in a single place. - - Instantiating a SimpleMovingAverage can be achieved as follows:: + 实例化 SimpleMovingAverage 可使用下列写法:: sma = MovingAverage.Simple(self.data, period) - Or using the shorter aliases:: + 也可以使用更短的别名:: sma = MovAv.SMA(self.data, period) - or with the full (forwards and backwards) names: + 或使用完整的正向/反向命名: sma = MovAv.SimpleMovingAverage(self.data, period) @@ -69,23 +67,23 @@ def register(cls, regcls): class MovAv(MovingAverage): - pass # alias + pass # 别名 class MetaMovAvBase(Indicator.__class__): - # Register any MovingAverage with the placeholder to allow the automatic - # creation of envelopes and oscillators + # 将所有 MovingAverage 注册到占位类,以便自动创建 envelope 和 oscillator def __new__(meta, name, bases, dct): - # Create the class + # 创建类 cls = super(MetaMovAvBase, meta).__new__(meta, name, bases, dct) MovingAverage.register(cls) - # return the class + # 返回类 return cls class MovingAverageBase(with_metaclass(MetaMovAvBase, Indicator)): + '''MovingAverage 的基类,用于统一 period 参数、绘图行为和自动登记逻辑。''' params = (('period', 30),) plotinfo = dict(subplot=False) diff --git a/backtrader/indicators/macd.py b/backtrader/indicators/macd.py index e12663fe2..575bf2bdd 100644 --- a/backtrader/indicators/macd.py +++ b/backtrader/indicators/macd.py @@ -26,13 +26,21 @@ class MACD(Indicator): ''' - Moving Average Convergence Divergence. Defined by Gerald Appel in the 70s. + Gerald Appel 在 20 世纪 70 年代定义的 Moving Average Convergence Divergence。 - It measures the distance of a short and a long term moving average to - try to identify the trend. + 它衡量短期与长期 Moving Average 之间的距离,用于尝试识别趋势。 - A second lagging moving average over the convergence-divergence should - provide a "signal" upon being crossed by the macd + 对 convergence-divergence 再做一次滞后 Moving Average,可在被 macd 穿越时 + 提供 "signal"。 + + Args: + period_me1: 短周期 Moving Average 周期。 + period_me2: 长周期 Moving Average 周期。 + period_signal: signal line 的平滑周期。 + movav: 用于计算的 Moving Average 类型。 + + Returns: + MACD: 输出 ``macd`` 与 ``signal`` line 的 indicator。 Formula: - macd = ema(data, me1_period) - ema(data, me2_period) @@ -40,6 +48,13 @@ class MACD(Indicator): See: - http://en.wikipedia.org/wiki/MACD + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(MACD) ''' lines = ('macd', 'signal',) params = (('period_me1', 12), ('period_me2', 26), ('period_signal', 9), @@ -65,14 +80,29 @@ def __init__(self): class MACDHisto(MACD): ''' - Subclass of MACD which adds a "histogram" of the difference between the - macd and signal lines + MACD 的子类,额外添加 macd 与 signal line 差值的 "histogram"。 + + Args: + period_me1: 短周期 Moving Average 周期。 + period_me2: 长周期 Moving Average 周期。 + period_signal: signal line 的平滑周期。 + movav: 用于计算的 Moving Average 类型。 + + Returns: + MACDHisto: 输出 ``macd``、``signal`` 与 ``histo`` line 的 indicator。 Formula: - histo = macd - signal See: - http://en.wikipedia.org/wiki/MACD + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(MACDHisto) ''' alias = ('MACDHistogram',) diff --git a/backtrader/indicators/momentum.py b/backtrader/indicators/momentum.py index 4f9d9ec40..6634fec5b 100644 --- a/backtrader/indicators/momentum.py +++ b/backtrader/indicators/momentum.py @@ -26,15 +26,26 @@ class Momentum(Indicator): ''' - Measures the change in price by calculating the difference between the - current price and the price from a given period ago + 通过计算当前价格与指定周期前价格的差值,衡量价格变化。 + Args: + period: 回看周期。 + + Returns: + Momentum: 输出 ``momentum`` line 的 indicator。 Formula: - momentum = data - data_period See: - http://en.wikipedia.org/wiki/Momentum_(technical_analysis) + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(Momentum, period=12) ''' lines = ('momentum',) params = (('period', 12),) @@ -47,20 +58,34 @@ def __init__(self): class MomentumOscillator(Indicator): ''' - Measures the ratio of change in prices over a period + 衡量指定周期内价格变化的比率。 + + Args: + period: 回看周期。 + band: 绘图时的参考线。 + + Returns: + MomentumOscillator: 输出 ``momosc`` line 的 indicator。 Formula: - mosc = 100 * (data / data_period) See: - http://ta.mql4.com/indicators/oscillators/momentum + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(MomentumOscillator, period=12) ''' alias = ('MomentumOsc',) - # Named output lines + # 命名输出 line lines = ('momosc',) - # Accepted parameters (and defaults) - + # 可接受参数及默认值 params = (('period', 12), ('band', 100.0)) @@ -78,20 +103,33 @@ def __init__(self): class RateOfChange(Indicator): ''' - Measures the ratio of change in prices over a period + 衡量指定周期内价格变化的相对比率。 + + Args: + period: 回看周期。 + + Returns: + RateOfChange: 输出 ``roc`` line 的 indicator。 Formula: - roc = (data - data_period) / data_period See: - http://en.wikipedia.org/wiki/Momentum_(technical_analysis) + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(RateOfChange, period=12) ''' alias = ('ROC',) - # Named output lines + # 命名输出 line lines = ('roc',) - # Accepted parameters (and defaults) - + # 可接受参数及默认值 params = (('period', 12),) def __init__(self): @@ -102,9 +140,15 @@ def __init__(self): class RateOfChange100(Indicator): ''' - Measures the ratio of change in prices over a period with base 100 + 以 100 为基准衡量指定周期内价格变化的相对比率。 - This is for example how ROC is defined in stockcharts + 例如 stockcharts 中的 ROC 即采用这种定义。 + + Args: + period: 回看周期。 + + Returns: + RateOfChange100: 输出 ``roc100`` line 的 indicator。 Formula: - roc = 100 * (data - data_period) / data_period @@ -112,13 +156,19 @@ class RateOfChange100(Indicator): See: - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:rate_of_change_roc_and_momentum + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(RateOfChange100, period=12) ''' alias = ('ROC100',) - # Named output lines + # 命名输出 line lines = ('roc100',) - # Accepted parameters (and defaults) + # 可接受参数及默认值 params = (('period', 12),) def __init__(self): diff --git a/backtrader/indicators/ols.py b/backtrader/indicators/ols.py index 639a84502..76910a8fc 100644 --- a/backtrader/indicators/ols.py +++ b/backtrader/indicators/ols.py @@ -31,12 +31,24 @@ class OLS_Slope_InterceptN(PeriodN): ''' - Calculates a linear regression using ``statsmodel.OLS`` (Ordinary least - squares) of data1 on data0 + 使用 ``statsmodel.OLS``(Ordinary least squares)计算 data1 对 data0 的线性回归。 - Uses ``pandas`` and ``statsmodels`` + 依赖 ``pandas`` 与 ``statsmodels``。 + + Args: + period: 回归窗口长度。 + + Returns: + OLS_Slope_InterceptN: 输出 ``slope`` 与 ``intercept`` line 的 indicator。 + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(OLS_Slope_InterceptN, period=10) ''' - _mindatas = 2 # ensure at least 2 data feeds are passed + _mindatas = 2 # 确保至少传入 2 个 data feed packages = ( ('pandas', 'pd'), @@ -59,11 +71,24 @@ def next(self): class OLS_TransformationN(PeriodN): ''' - Calculates the ``zscore`` for data0 and data1. Although it doesn't directly - uses any external package it relies on ``OLS_SlopeInterceptN`` which uses - ``pandas`` and ``statsmodels`` + 计算 data0 与 data1 的 ``zscore``。该类不直接使用外部包,但依赖使用 + ``pandas`` 与 ``statsmodels`` 的 ``OLS_Slope_InterceptN``。 + + Args: + period: 回归和统计窗口长度。 + + Returns: + OLS_TransformationN: 输出 ``spread``、``spread_mean``、``spread_std`` + 与 ``zscore`` line 的 indicator。 + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(OLS_TransformationN, period=10) ''' - _mindatas = 2 # ensure at least 2 data feeds are passed + _mindatas = 2 # 确保至少传入 2 个 data feed lines = ('spread', 'spread_mean', 'spread_std', 'zscore',) params = (('period', 10),) @@ -80,11 +105,24 @@ def __init__(self): class OLS_BetaN(PeriodN): ''' - Calculates a regression of data1 on data0 using ``pandas.ols`` + 使用 ``pandas.ols`` 计算 data1 对 data0 的回归 beta。 - Uses ``pandas`` + 依赖 ``pandas``。 + + Args: + period: 回归窗口长度。 + + Returns: + OLS_BetaN: 输出 ``beta`` line 的 indicator。 + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(OLS_BetaN, period=10) ''' - _mindatas = 2 # ensure at least 2 data feeds are passed + _mindatas = 2 # 确保至少传入 2 个 data feed packages = ( ('pandas', 'pd'), @@ -101,12 +139,25 @@ def next(self): class CointN(PeriodN): ''' - Calculates the score (coint_t) and pvalue for a given ``period`` for the - data feeds + 为传入 data feed 在给定 ``period`` 上计算协整 score(coint_t)与 pvalue。 + + 依赖 ``pandas`` 与 ``statsmodels``(用于 ``coint``)。 + + Args: + period: 协整检验窗口长度。 + trend: 传给 ``statsmodels.tsa.stattools.coint`` 的 trend 参数。 + + Returns: + CointN: 输出 ``score`` 与 ``pvalue`` line 的 indicator。 + + --- + 交互界面使用示范: - Uses ``pandas`` and ``statsmodels`` (for ``coint``) + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(CointN, period=10) ''' - _mindatas = 2 # ensure at least 2 data feeds are passed + _mindatas = 2 # 确保至少传入 2 个 data feed packages = ( ('pandas', 'pd'), # import pandas as pd @@ -118,7 +169,7 @@ class CointN(PeriodN): lines = ('score', 'pvalue',) params = ( ('period', 10), - ('trend', 'c'), # see statsmodel.tsa.statttools + ('trend', 'c'), # 见 statsmodel.tsa.statttools ) def next(self): diff --git a/backtrader/indicators/oscillator.py b/backtrader/indicators/oscillator.py index 8155c3296..724bdadcc 100644 --- a/backtrader/indicators/oscillator.py +++ b/backtrader/indicators/oscillator.py @@ -29,11 +29,10 @@ class OscillatorMixIn(Indicator): ''' - MixIn class to create a subclass with another indicator. The main line of - that indicator will be substracted from the other base class main line - creating an oscillator + Oscillator 的 MixIn 基类,用于与另一个 indicator 组合生成 oscillator。 + 该 indicator 的主 line 会从另一个基类的主 line 中扣除。 - The usage is: + 用法: - Class XXXOscillator(XXX, OscillatorMixIn) @@ -57,27 +56,42 @@ def __init__(self): class Oscillator(Indicator): ''' - Oscillation of a given data around another data + 给定 data 围绕另一个 data 的 oscillation。 + + Args: + data: 单 data 模式下为带有原始 datas 的 Lines 对象;双 data 模式下为基准 + data。 + data1: 双 data 模式下用于计算 oscillation 的另一个 data。 + + Returns: + Oscillator: 输出 ``osc`` line 的 indicator。 Datas: - This indicator can accept 1 or 2 datas for the calculation. + 该 indicator 可接受 1 或 2 个 data 进行计算。 - - If 1 data is provided, it must be a complex "Lines" object (indicator) - which also has "datas". Example: A moving average + - 如果提供 1 个 data,它必须是同时持有 ``datas`` 的复杂 "Lines" 对象 + (indicator)。例如:Moving Average。 - The calculated oscillation will be that of the Moving Average (in the - example) around the data that was used for the average calculation + 计算结果表示该 Moving Average 围绕其计算所用原始 data 的 oscillation。 - - If 2 datas are provided the calculated oscillation will be that of the - 2nd data around the 1st data + - 如果提供 2 个 data,则表示第 2 个 data 围绕第 1 个 data 的 oscillation。 Formula: - 1 data -> osc = data.data - data - 2 datas -> osc = data0 - data1 + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> from backtrader.indicators import SimpleMovingAverage + >>> cerebro = Cerebro() + >>> cerebro.addindicator(SimpleMovingAverage) + >>> cerebro.addindicator(Oscillator) ''' lines = ('osc',) - # Have a default value which can be later modified if needed + # 提供默认值,后续可按需修改 plotlines = dict(_0=dict(_name='osc')) def _plotinit(self): @@ -100,13 +114,13 @@ def __init__(self): self.lines[0] = datasrc - self.dataosc -# Automatic creation of Oscillating Lines +# 自动创建 Oscillating Lines for movav in MovingAverage._movavs[1:]: _newclsdoc = ''' - Oscillation of a %s around its data + %s 围绕其 data 的 oscillation。 ''' - # Skip aliases - they will be created automatically + # 跳过 alias,它们会自动创建 if getattr(movav, 'aliased', ''): continue diff --git a/backtrader/indicators/percentchange.py b/backtrader/indicators/percentchange.py index c1013db04..0dc959d34 100644 --- a/backtrader/indicators/percentchange.py +++ b/backtrader/indicators/percentchange.py @@ -29,16 +29,28 @@ class PercentChange(Indicator): ''' - Measures the perccentage change of the current value with respect to that - of period bars ago + 计算当前值相对 ``period`` 个 bar 前的 percentage change。 + + Args: + period: 回看周期。 + + Returns: + PercentChange: 输出 ``pctchange`` line 的 indicator。 + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(PercentChange, period=30) ''' alias = ('PctChange',) lines = ('pctchange',) - # Fancy plotting name + # 更适合绘图显示的名称 plotlines = dict(pctchange=dict(_name='%change')) - # update value to standard for Moving Averages + # 使用与 Moving Averages 统一的 period 参数名 params = (('period', 30),) def __init__(self): diff --git a/backtrader/indicators/percentrank.py b/backtrader/indicators/percentrank.py index 3d4f14f9c..80f8d74f5 100644 --- a/backtrader/indicators/percentrank.py +++ b/backtrader/indicators/percentrank.py @@ -31,8 +31,21 @@ class PercentRank(BaseApplyN): ''' - Measures the percent rank of the current value with respect to that of - period bars ago + 计算当前值在最近 ``period`` 个 bar 中的 percent rank。 + + Args: + period: 统计窗口周期。 + func: 对窗口数据执行 percent rank 计算的函数。 + + Returns: + PercentRank: 输出 ``pctrank`` line 的 indicator。 + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(PercentRank, period=50) ''' alias = ('PctRank',) lines = ('pctrank',) diff --git a/backtrader/indicators/pivotpoint.py b/backtrader/indicators/pivotpoint.py index 750b4ab80..eda45d71b 100644 --- a/backtrader/indicators/pivotpoint.py +++ b/backtrader/indicators/pivotpoint.py @@ -26,12 +26,10 @@ class PivotPoint(Indicator): ''' - Defines a level of significance by taking into account the average of price - bar components of the past period of a larger timeframe. For example when - operating with days, the values are taking from the already "past" month - fixed prices. + 通过更大 timeframe 的上一周期价格 bar 组件均值定义关键价位。例如以日线交易时, + 可使用已经过去的月线固定价格计算 pivot。 - Example of using this indicator: + 使用示例: data = btfeeds.ADataFeed(dataname=x, timeframe=bt.TimeFrame.Days) cerebro.adddata(data) @@ -41,15 +39,22 @@ class PivotPoint(Indicator): pivotindicator = btind.PivotPoiont(self.data1) # the resampled data - The indicator will try to automatically plo to the non-resampled data. To - disable this behavior use the following during construction: + indicator 会尝试自动绘制到非 resampled data 上。若要关闭该行为,构造时使用: - _autoplot=False Note: - The example shows *days* and *months*, but any combination of timeframes - can be used. See the literature for recommended combinations + 示例使用 *days* 与 *months*,但可使用任意 timeframe 组合;推荐组合见相关资料。 + + Args: + open: 是否将 open 加入 pivot point 计算。 + close: 是否在计算中重复使用 close。 + _autoplot: 是否尝试绘制到真实目标 data 上。 + + Returns: + PivotPoint: 输出 ``p``、``s1``、``s2``、``r1`` 与 ``r2`` line 的 + indicator。 Formula: - pivot = (h + l + c) / 3 # variants duplicate close or add open @@ -61,27 +66,34 @@ class PivotPoint(Indicator): See: - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:pivot_points - https://en.wikipedia.org/wiki/Pivot_point_(technical_analysis) + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(PivotPoint, _autoplot=False) ''' lines = ('p', 's1', 's2', 'r1', 'r2',) plotinfo = dict(subplot=False) params = ( - ('open', False), # add opening price to the pivot point - ('close', False), # use close twice in the calcs - ('_autoplot', True), # attempt to plot on real target data + ('open', False), # 将开盘价加入 pivot point + ('close', False), # 在计算中重复使用 close + ('_autoplot', True), # 尝试绘制到真实目标 data 上 ) def _plotinit(self): - # Try to plot to the actual timeframe master + # 尝试绘制到实际 timeframe master if self.p._autoplot: if hasattr(self.data, 'data'): self.plotinfo.plotmaster = self.data.data def __init__(self): o = self.data.open - h = self.data.high # current high - l = self.data.low # current low - c = self.data.close # current close + h = self.data.high # 当前 high + l = self.data.low # 当前 low + c = self.data.close # 当前 close if self.p.close: self.lines.p = p = (h + l + 2.0 * c) / 4.0 @@ -96,23 +108,20 @@ def __init__(self): self.lines.s2 = p - (h - l) self.lines.r2 = p + (h - l) - super(PivotPoint, self).__init__() # enable coopertive inheritance + super(PivotPoint, self).__init__() # 启用协作式继承 if self.p._autoplot: - self.plotinfo.plot = False # disable own plotting - self() # Coupler to follow real object + self.plotinfo.plot = False # 禁用自身绘图 + self() # Coupler 跟随真实对象 class FibonacciPivotPoint(Indicator): ''' - Defines a level of significance by taking into account the average of price - bar components of the past period of a larger timeframe. For example when - operating with days, the values are taking from the already "past" month - fixed prices. + 通过更大 timeframe 的上一周期价格 bar 组件均值定义关键价位。 - Fibonacci levels (configurable) are used to define the support/resistance levels + 使用可配置的 Fibonacci level 定义 support/resistance level。 - Example of using this indicator: + 使用示例: data = btfeeds.ADataFeed(dataname=x, timeframe=bt.TimeFrame.Days) cerebro.adddata(data) @@ -122,15 +131,25 @@ class FibonacciPivotPoint(Indicator): pivotindicator = btind.FibonacciPivotPoiont(self.data1) # the resampled data - The indicator will try to automatically plo to the non-resampled data. To - disable this behavior use the following during construction: + indicator 会尝试自动绘制到非 resampled data 上。若要关闭该行为,构造时使用: - _autoplot=False Note: - The example shows *days* and *months*, but any combination of timeframes - can be used. See the literature for recommended combinations + 示例使用 *days* 与 *months*,但可使用任意 timeframe 组合;推荐组合见相关资料。 + + Args: + open: 是否将 open 加入 pivot point 计算。 + close: 是否在计算中重复使用 close。 + _autoplot: 是否尝试绘制到真实目标 data 上。 + level1: 第 1 个 Fibonacci level。 + level2: 第 2 个 Fibonacci level。 + level3: 第 3 个 Fibonacci level。 + + Returns: + FibonacciPivotPoint: 输出 ``p``、``s1``、``s2``、``s3``、``r1``、 + ``r2`` 与 ``r3`` line 的 indicator。 Formula: - pivot = (h + l + c) / 3 # variants duplicate close or add open @@ -143,29 +162,36 @@ class FibonacciPivotPoint(Indicator): See: - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:pivot_points + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(FibonacciPivotPoint, _autoplot=False) ''' lines = ('p', 's1', 's2', 's3', 'r1', 'r2', 'r3') plotinfo = dict(subplot=False) params = ( - ('open', False), # add opening price to the pivot point - ('close', False), # use close twice in the calcs - ('_autoplot', True), # attempt to plot on real target data + ('open', False), # 将开盘价加入 pivot point + ('close', False), # 在计算中重复使用 close + ('_autoplot', True), # 尝试绘制到真实目标 data 上 ('level1', 0.382), ('level2', 0.618), ('level3', 1.0), ) def _plotinit(self): - # Try to plot to the actual timeframe master + # 尝试绘制到实际 timeframe master if self.p._autoplot: if hasattr(self.data, 'data'): self.plotinfo.plotmaster = self.data.data def __init__(self): o = self.data.open - h = self.data.high # current high - l = self.data.low # current high - c = self.data.close # current high + h = self.data.high # 当前 high + l = self.data.low # 当前 low + c = self.data.close # 当前 close if self.p.close: self.lines.p = p = (h + l + 2.0 * c) / 4.0 @@ -185,18 +211,15 @@ def __init__(self): super(FibonacciPivotPoint, self).__init__() if self.p._autoplot: - self.plotinfo.plot = False # disable own plotting - self() # Coupler to follow real object + self.plotinfo.plot = False # 禁用自身绘图 + self() # Coupler 跟随真实对象 class DemarkPivotPoint(Indicator): ''' - Defines a level of significance by taking into account the average of price - bar components of the past period of a larger timeframe. For example when - operating with days, the values are taking from the already "past" month - fixed prices. + 通过更大 timeframe 的上一周期价格 bar 组件均值定义 DeMark pivot 关键价位。 - Example of using this indicator: + 使用示例: data = btfeeds.ADataFeed(dataname=x, timeframe=bt.TimeFrame.Days) cerebro.adddata(data) @@ -206,15 +229,21 @@ class DemarkPivotPoint(Indicator): pivotindicator = btind.DemarkPivotPoiont(self.data1) # the resampled data - The indicator will try to automatically plo to the non-resampled data. To - disable this behavior use the following during construction: + indicator 会尝试自动绘制到非 resampled data 上。若要关闭该行为,构造时使用: - _autoplot=False Note: - The example shows *days* and *months*, but any combination of timeframes - can be used. See the literature for recommended combinations + 示例使用 *days* 与 *months*,但可使用任意 timeframe 组合;推荐组合见相关资料。 + + Args: + open: 是否将 open 加入 pivot point 计算。 + close: 是否在计算中重复使用 close。 + _autoplot: 是否尝试绘制到真实目标 data 上。 + + Returns: + DemarkPivotPoint: 输出 ``p``、``s1`` 与 ``r1`` line 的 indicator。 Formula: - if close < open x = high + (2 x low) + close @@ -230,20 +259,27 @@ class DemarkPivotPoint(Indicator): See: - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:pivot_points + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(DemarkPivotPoint, _autoplot=False) ''' lines = ('p', 's1', 'r1',) plotinfo = dict(subplot=False) params = ( - ('open', False), # add opening price to the pivot point - ('close', False), # use close twice in the calcs - ('_autoplot', True), # attempt to plot on real target data + ('open', False), # 将开盘价加入 pivot point + ('close', False), # 在计算中重复使用 close + ('_autoplot', True), # 尝试绘制到真实目标 data 上 ('level1', 0.382), ('level2', 0.618), ('level3', 1.0), ) def _plotinit(self): - # Try to plot to the actual timeframe master + # 尝试绘制到实际 timeframe master if self.p._autoplot: if hasattr(self.data, 'data'): self.plotinfo.plotmaster = self.data.data @@ -262,5 +298,5 @@ def __init__(self): super(DemarkPivotPoint, self).__init__() if self.p._autoplot: - self.plotinfo.plot = False # disable own plotting - self() # Coupler to follow real object + self.plotinfo.plot = False # 禁用自身绘图 + self() # Coupler 跟随真实对象 diff --git a/backtrader/indicators/prettygoodoscillator.py b/backtrader/indicators/prettygoodoscillator.py index 7db3ef0fa..f96b0861b 100644 --- a/backtrader/indicators/prettygoodoscillator.py +++ b/backtrader/indicators/prettygoodoscillator.py @@ -27,18 +27,21 @@ class PrettyGoodOscillator(Indicator): ''' - The "Pretty Good Oscillator" (PGO) by Mark Johnson measures the distance of - the current close from its simple moving average of period - Average), expressed in terms of an average true range (see Average True - Range) over a similar period. + Mark Johnson 提出的 "Pretty Good Oscillator" (PGO),用于衡量当前 close + 与同周期 Simple Moving Average 的距离,并用同类周期的 Average True Range + 表达该距离。 - So for instance a PGO value of +2.5 would mean the current close is 2.5 - average days' range above the SMA. + 例如 PGO 为 +2.5,表示当前 close 高于 SMA 的距离约为 2.5 个平均日内波幅。 - Johnson's approach was to use it as a breakout system for longer term - trades. If the PGO rises above 3.0 then go long, or below -3.0 then go - short, and in both cases exit on returning to zero (which is a close back - at the SMA). + Johnson 的用法偏向较长期的突破系统:PGO 上穿 3.0 可做多,下穿 -3.0 可做空, + 两种情况下都在回到 0(即 close 回到 SMA 附近)时退出。 + + Args: + period: 计算 Moving Average 与 ATR 的周期。 + _movav: 用于计算中线的 Moving Average 类型。 + + Returns: + PrettyGoodOscillator: 输出 ``pgo`` line 的 indicator。 Formula: - pgo = (data.close - sma(data, period)) / atr(data, period) @@ -46,6 +49,12 @@ class PrettyGoodOscillator(Indicator): See also: - http://user42.tuxfamily.org/chart/manual/Pretty-Good-Oscillator.html + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(PrettyGoodOscillator, period=14) ''' alias = ('PGO', 'PrettyGoodOsc',) lines = ('pgo',) diff --git a/backtrader/indicators/priceoscillator.py b/backtrader/indicators/priceoscillator.py index 083f0ff4c..a830e52e0 100644 --- a/backtrader/indicators/priceoscillator.py +++ b/backtrader/indicators/priceoscillator.py @@ -25,6 +25,7 @@ class _PriceOscBase(Indicator): + '''Price Oscillator 的基类,用于统一短/长 Moving Average 差值计算。''' params = (('period1', 12), ('period2', 26), ('_movav', MovAv.Exponential),) @@ -40,14 +41,28 @@ def __init__(self): class PriceOscillator(_PriceOscBase): ''' - Shows the difference between a short and long exponential moving - averages expressed in points. + 显示短周期与长周期 Exponential Moving Average 的差值,以点数表达。 + + Args: + period1: 短周期 Moving Average 周期。 + period2: 长周期 Moving Average 周期。 + _movav: 用于计算的 Moving Average 类型。 + + Returns: + PriceOscillator: 输出 ``po`` line 的 indicator。 Formula: - po = ema(short) - ema(long) See: - http://www.metastock.com/Customer/Resources/TAAZ/?c=3&p=94 + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(PriceOscillator) ''' alias = ('PriceOsc', 'AbsolutePriceOscillator', 'APO', 'AbsPriceOsc',) lines = ('po',) @@ -55,19 +70,33 @@ class PriceOscillator(_PriceOscBase): class PercentagePriceOscillator(_PriceOscBase): ''' - Shows the difference between a short and long exponential moving - averages expressed in percentage. The MACD does the same but expressed in - absolute points. + 显示短周期与长周期 Exponential Moving Average 的差值,以百分比表达。 + MACD 做的是类似计算,但以绝对点数表达。 + + 用百分比表达差值,可以在底层价格水平差异很大时比较不同时点的指标值。 + + Args: + period1: 短周期 Moving Average 周期。 + period2: 长周期 Moving Average 周期。 + period_signal: signal line 的平滑周期。 + _movav: 用于计算的 Moving Average 类型。 - Expressing the difference in percentage allows to compare the indicator at - different points in time when the underlying value has significatnly - different values. + Returns: + PercentagePriceOscillator: 输出 ``ppo``、``signal`` 与 ``histo`` line + 的 indicator。 Formula: - po = 100 * (ema(short) - ema(long)) / ema(long) See: - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:price_oscillators_ppo + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(PercentagePriceOscillator) ''' _long = True @@ -90,23 +119,34 @@ def __init__(self): class PercentagePriceOscillatorShort(PercentagePriceOscillator): ''' - Shows the difference between a short and long exponential moving - averages expressed in percentage. The MACD does the same but expressed in - absolute points. + PercentagePriceOscillator 的短周期分母版本,以短周期 EMA 作为百分比计算分母。 + + 用百分比表达差值,可以在底层价格水平差异很大时比较不同时点的指标值。 - Expressing the difference in percentage allows to compare the indicator at - different points in time when the underlying value has significatnly - different values. + 大部分在线资料使用长周期 EMA 作为分母;MetaStock 等资料使用短周期 EMA。 - Most on-line literature shows the percentage calculation having the long - exponential moving average as the denominator. Some sources like MetaStock - use the short one. + Args: + period1: 短周期 Moving Average 周期。 + period2: 长周期 Moving Average 周期。 + period_signal: signal line 的平滑周期。 + _movav: 用于计算的 Moving Average 类型。 + + Returns: + PercentagePriceOscillatorShort: 输出 ``ppo``、``signal`` 与 ``histo`` + line 的 indicator。 Formula: - po = 100 * (ema(short) - ema(long)) / ema(short) See: - http://www.metastock.com/Customer/Resources/TAAZ/?c=3&p=94 + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(PercentagePriceOscillatorShort) ''' _long = False alias = ('PPOShort', 'PercPriceOscShort',) diff --git a/backtrader/indicators/psar.py b/backtrader/indicators/psar.py index b03c899e6..42d4ab3ce 100644 --- a/backtrader/indicators/psar.py +++ b/backtrader/indicators/psar.py @@ -28,6 +28,7 @@ class _SarStatus(object): + '''ParabolicSAR 的内部状态对象,用于保存当前/上一轮趋势状态。''' sar = None tr = None af = 0.0 @@ -44,23 +45,36 @@ def __str__(self): class ParabolicSAR(PeriodN): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"* for the RSI + J. Welles Wilder, Jr. 于 1978 年在 *"New Concepts in Technical Trading + Systems"* 中定义的 Parabolic SAR。 - SAR stands for *Stop and Reverse* and the indicator was meant as a signal - for entry (and reverse) + SAR 表示 *Stop and Reverse*,该 indicator 设计为入场与反转信号。 - How to select the 1st signal is left unspecified in the book and the - increase/decrease of bars + 原书未明确说明如何选择第一个信号以及 bar 增减的处理细节。 + + Args: + period: 开始显示数值前的最小周期。 + af: acceleration factor 初始增量。 + afmax: acceleration factor 最大值。 + + Returns: + ParabolicSAR: 输出 ``psar`` line 的 indicator。 See: - https://en.wikipedia.org/wiki/Parabolic_SAR - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:parabolic_sar + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(ParabolicSAR) ''' alias = ('PSAR',) lines = ('psar',) params = ( - ('period', 2), # when to start showing values + ('period', 2), # 何时开始显示数值 ('af', 0.02), ('afmax', 0.20), ) @@ -74,97 +88,92 @@ class ParabolicSAR(PeriodN): def prenext(self): if len(self) == 1: - self._status = [] # empty status - return # not enough data to do anything + self._status = [] # 空状态 + return # 数据不足,无法计算 elif len(self) == 2: - self.nextstart() # kickstart calculation + self.nextstart() # 启动计算 else: - self.next() # regular calc + self.next() # 常规计算 - self.lines.psar[0] = float('NaN') # no return yet still prenext + self.lines.psar[0] = float('NaN') # 仍处于 prenext,暂不返回有效值 def nextstart(self): - if self._status: # some states have been calculated - self.next() # delegate + if self._status: # 已经计算出部分状态 + self.next() # 委托给 next return - # Prepare a status holding array, for current and previous lengths + # 准备状态数组,分别保存当前长度和上一长度的状态 self._status = [_SarStatus(), _SarStatus()] - # Start by looking if price has gone up/down (close) in the 2nd day to - # get an *entry* signal and configure the values as they would have - # been in the previous trend, including a sar value which is - # immediately invalidated in next, which reverses and sets the trend to - # the actual up/down value calculated with the close - # Put the 4 status variables in a Status holder - plenidx = (len(self) - 1) % 2 # previous length index (0 or 1) + # 先观察第 2 天 close 的涨跌来获得 entry 信号,并按“上一趋势”的形态设置值。 + # 其中 sar 会在 next 中立即失效,随后反转并根据 close 计算出的实际涨跌设置趋势。 + # 4 个状态变量放入状态持有对象。 + plenidx = (len(self) - 1) % 2 # 上一长度索引(0 或 1) status = self._status[plenidx] - # Calculate the status for previous length + # 计算上一长度的状态 status.sar = (self.data.high[0] + self.data.low[0]) / 2.0 status.af = self.p.af - if self.data.close[0] >= self.data.close[-1]: # uptrend - status.tr = not True # uptrend when reversed - status.ep = self.data.low[-1] # ep from prev trend + if self.data.close[0] >= self.data.close[-1]: # 上升趋势 + status.tr = not True # 反转后为上升趋势 + status.ep = self.data.low[-1] # 来自上一趋势的 ep else: - status.tr = not False # downtrend when reversed - status.ep = self.data.high[-1] # ep from prev trend + status.tr = not False # 反转后为下降趋势 + status.ep = self.data.high[-1] # 来自上一趋势的 ep - # With the fake prev trend in place and a sar which will be invalidated - # go to next to get the calculation done + # 带着伪造的上一趋势和即将失效的 sar 进入 next,完成正式计算 self.next() def next(self): hi = self.data.high[0] lo = self.data.low[0] - plenidx = (len(self) - 1) % 2 # previous length index (0 or 1) - status = self._status[plenidx] # use prev status for calculations + plenidx = (len(self) - 1) % 2 # 上一长度索引(0 或 1) + status = self._status[plenidx] # 使用上一状态进行计算 tr = status.tr sar = status.sar - # Check if the sar penetrated the price to switch the trend + # 检查 sar 是否穿透价格以切换趋势 if (tr and sar >= lo) or (not tr and sar <= hi): - tr = not tr # reverse the trend - sar = status.ep # new sar is prev SIP (Significant price) - ep = hi if tr else lo # select new SIP / Extreme Price - af = self.p.af # reset acceleration factor + tr = not tr # 反转趋势 + sar = status.ep # 新 sar 使用上一 SIP(Significant price) + ep = hi if tr else lo # 选择新的 SIP / Extreme Price + af = self.p.af # 重置 acceleration factor - else: # use the precalculated values + else: # 使用预计算值 ep = status.ep af = status.af - # Update sar value for today + # 更新今日 sar 值 self.lines.psar[0] = sar - # Update ep and af if needed - if tr: # long trade + # 按需更新 ep 和 af + if tr: # 多头趋势 if hi > ep: ep = hi af = min(af + self.p.af, self.p.afmax) - else: # downtrend + else: # 下降趋势 if lo < ep: ep = lo af = min(af + self.p.af, self.p.afmax) - sar = sar + af * (ep - sar) # calculate the sar for tomorrow + sar = sar + af * (ep - sar) # 计算明日 sar - # make sure sar doesn't go into hi/lows - if tr: # long trade + # 确保 sar 不进入 high/low 区间 + if tr: # 多头趋势 lo1 = self.data.low[-1] if sar > lo or sar > lo1: - sar = min(lo, lo1) # sar not above last 2 lows -> lower + sar = min(lo, lo1) # sar 不高于最近 2 个 low -> 取较低值 else: hi1 = self.data.high[-1] if sar < hi or sar < hi1: - sar = max(hi, hi1) # sar not below last 2 highs -> highest + sar = max(hi, hi1) # sar 不低于最近 2 个 high -> 取较高值 - # new status has been calculated, keep it in current length - # will be used when length moves forward + # 新状态已经计算完成,保存在当前长度中,供下一次长度推进时使用 newstatus = self._status[not plenidx] newstatus.tr = tr newstatus.sar = sar diff --git a/backtrader/indicators/rmi.py b/backtrader/indicators/rmi.py index e9a950a19..c31d1e1c5 100644 --- a/backtrader/indicators/rmi.py +++ b/backtrader/indicators/rmi.py @@ -26,29 +26,38 @@ class RelativeMomentumIndex(RSI): ''' - Description: - The Relative Momentum Index was developed by Roger Altman and was - introduced in his article in the February, 1993 issue of Technical Analysis - of Stocks & Commodities magazine. + Roger Altman 开发的 Relative Momentum Index,并在 1993 年 2 月 + *Technical Analysis of Stocks & Commodities* 杂志文章中介绍。 - While your typical RSI counts up and down days from close to close, the - Relative Momentum Index counts up and down days from the close relative to - a close x number of days ago. The result is an RSI that is a bit smoother. + 普通 RSI 统计 close 到 close 的涨跌日,Relative Momentum Index 则统计当前 + close 相对若干日前 close 的涨跌,因此结果会比 RSI 更平滑一些。 - Usage: - Use in the same way you would any other RSI . There are overbought and - oversold zones, and can also be used for divergence and trend analysis. + 用法与 RSI 类似,可观察 overbought/oversold 区域,也可用于 divergence + 与趋势分析。 + + Args: + period: RSI 平滑周期。 + lookback: 用来比较历史 close 的回看周期。 + + Returns: + RelativeMomentumIndex: 输出 ``rmi`` line 别名的 RSI 派生 indicator。 See: - https://www.marketvolume.com/technicalanalysis/relativemomentumindex.asp - https://www.tradingview.com/script/UCm7fIvk-FREE-INDICATOR-Relative-Momentum-Index-RMI/ - https://www.prorealcode.com/prorealtime-indicators/relative-momentum-index-rmi/ + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(RelativeMomentumIndex, period=20, lookback=5) ''' alias = ('RMI', ) - linealias = (('rsi', 'rmi',),) # add an alias for this class rmi -> rsi - plotlines = dict(rsi=dict(_name='rmi')) # change line plotting name + linealias = (('rsi', 'rmi',),) # 为该类添加 rmi -> rsi 的 line 别名 + plotlines = dict(rsi=dict(_name='rmi')) # 修改绘图时显示的 line 名称 params = ( ('period', 20), @@ -56,7 +65,7 @@ class RelativeMomentumIndex(RSI): ) def _plotlabel(self): - # override to always print the lookback label and do it before movav + # 覆盖标签逻辑,始终显示 lookback,并将其放在 movav 之前 plabels = [self.p.period] plabels += [self.p.lookback] plabels += [self.p.movav] * self.p.notdefault('movav') diff --git a/backtrader/indicators/rsi.py b/backtrader/indicators/rsi.py index 56cd9210b..213f75349 100644 --- a/backtrader/indicators/rsi.py +++ b/backtrader/indicators/rsi.py @@ -27,17 +27,29 @@ class UpDay(Indicator): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"* for the RSI + J. Welles Wilder, Jr. 于 1978 年在 *"New Concepts in Technical Trading + Systems"* 中为 RSI 定义的 UpDay。 - Records days which have been "up", i.e.: the close price has been - higher than the day before. + 记录 "up" day,即 close 高于前一日的情况。 + + Args: + period: 比较前值的回看周期。 + + Returns: + UpDay: 输出 ``upday`` line 的 indicator。 Formula: - upday = max(close - close_prev, 0) See: - http://en.wikipedia.org/wiki/Relative_strength_index + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(UpDay) ''' lines = ('upday',) params = (('period', 1),) @@ -49,17 +61,29 @@ def __init__(self): class DownDay(Indicator): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"* for the RSI + J. Welles Wilder, Jr. 于 1978 年在 *"New Concepts in Technical Trading + Systems"* 中为 RSI 定义的 DownDay。 - Records days which have been "down", i.e.: the close price has been - lower than the day before. + 记录 "down" day,即 close 低于前一日的情况。 + + Args: + period: 比较前值的回看周期。 + + Returns: + DownDay: 输出 ``downday`` line 的 indicator。 Formula: - downday = max(close_prev - close, 0) See: - http://en.wikipedia.org/wiki/Relative_strength_index + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(DownDay) ''' lines = ('downday',) params = (('period', 1),) @@ -71,20 +95,32 @@ def __init__(self): class UpDayBool(Indicator): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"* for the RSI + J. Welles Wilder, Jr. 于 1978 年在 *"New Concepts in Technical Trading + Systems"* 中为 RSI 定义的布尔版 UpDay。 - Records days which have been "up", i.e.: the close price has been - higher than the day before. + 记录 "up" day,即 close 高于前一日的情况。 + + Args: + period: 比较前值的回看周期。 + + Returns: + UpDayBool: 输出布尔型 ``upday`` line 的 indicator。 Note: - - This version returns a bool rather than the difference + - 该版本返回 bool,而不是差值。 Formula: - upday = close > close_prev See: - http://en.wikipedia.org/wiki/Relative_strength_index + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(UpDayBool) ''' lines = ('upday',) params = (('period', 1),) @@ -96,20 +132,32 @@ def __init__(self): class DownDayBool(Indicator): ''' - Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"* for the RSI + J. Welles Wilder, Jr. 于 1978 年在 *"New Concepts in Technical Trading + Systems"* 中为 RSI 定义的布尔版 DownDay。 - Records days which have been "down", i.e.: the close price has been - lower than the day before. + 记录 "down" day,即 close 低于前一日的情况。 + + Args: + period: 比较前值的回看周期。 + + Returns: + DownDayBool: 输出布尔型 ``downday`` line 的 indicator。 Note: - - This version returns a bool rather than the difference + - 该版本返回 bool,而不是差值。 Formula: - downday = close_prev > close See: - http://en.wikipedia.org/wiki/Relative_strength_index + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(DownDayBool) ''' lines = ('downday',) params = (('period', 1),) @@ -120,12 +168,24 @@ def __init__(self): class RelativeStrengthIndex(Indicator): - '''Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in - Technical Trading Systems"*. + '''J. Welles Wilder, Jr. 于 1978 年在 *"New Concepts in Technical Trading + Systems"* 中定义的 Relative Strength Index。 - It measures momentum by calculating the ration of higher closes and - lower closes after having been smoothed by an average, normalizing - the result between 0 and 100 + 它先对上涨 close 与下跌 close 进行平均平滑,再计算二者比例,用 0 到 100 + 的范围表达 momentum。 + + Args: + period: RSI 平滑周期。 + movav: 用于平滑 up/down day 的 Moving Average 类型。 + upperband: 绘图时的上轨参考线。 + lowerband: 绘图时的下轨参考线。 + safediv: 是否处理 ``0 / 0`` 与 ``x / 0`` 的特殊除法情况。 + safehigh: ``x / 0`` 时使用的 RSI 值。 + safelow: ``0 / 0`` 时使用的 RSI 值。 + lookback: up/down day 比较的回看周期。 + + Returns: + RelativeStrengthIndex: 输出 ``rsi`` line 的 indicator。 Formula: - up = upday(data) @@ -135,22 +195,25 @@ class RelativeStrengthIndex(Indicator): - rs = maup / madown - rsi = 100 - 100 / (1 + rs) - The moving average used is the one originally defined by Wilder, - the SmoothedMovingAverage + 默认 Moving Average 使用 Wilder 原始定义中的 SmoothedMovingAverage。 See: - http://en.wikipedia.org/wiki/Relative_strength_index Notes: - - ``safediv`` (default: False) If this parameter is True the division - rs = maup / madown will be checked for the special cases in which a - ``0 / 0`` or ``x / 0`` division will happen + - ``safediv`` 为 True 时,会检查 ``rs = maup / madown`` 中可能出现的 + ``0 / 0`` 或 ``x / 0`` 特殊情况。 + + - ``safehigh`` 会作为 ``x / 0`` 情况下的 RSI 值。 + + - ``safelow`` 会作为 ``0 / 0`` 情况下的 RSI 值。 - - ``safehigh`` (default: 100.0) will be used as RSI value for the - ``x / 0`` case + --- + 交互界面使用示范: - - ``safelow`` (default: 50.0) will be used as RSI value for the - ``0 / 0`` case + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(RelativeStrengthIndex, period=14) ''' alias = ('RSI', 'RSI_SMMA', 'RSI_Wilder',) @@ -201,21 +264,49 @@ def _rscalc(self, rsi): class RSI_Safe(RSI): ''' - Subclass of RSI which changes parameers ``safediv`` to ``True`` as the - default value + RSI 的子类,将 ``safediv`` 默认值改为 ``True``。 + + Args: + period: RSI 平滑周期。 + movav: 用于平滑 up/down day 的 Moving Average 类型。 + lookback: up/down day 比较的回看周期。 + + Returns: + RSI_Safe: 输出 ``rsi`` line 的安全除法版 RSI indicator。 See: - http://en.wikipedia.org/wiki/Relative_strength_index + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(RSI_Safe) ''' params = (('safediv', True),) class RSI_SMA(RSI): ''' - Uses a SimpleMovingAverage as described in Wikipedia and other soures + 使用 Wikipedia 和其他资料中描述的 SimpleMovingAverage 版本 RSI。 + + Args: + period: RSI 平滑周期。 + lookback: up/down day 比较的回看周期。 + + Returns: + RSI_SMA: 输出 ``rsi`` line 的 SimpleMovingAverage 版 RSI indicator。 See: - http://en.wikipedia.org/wiki/Relative_strength_index + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(RSI_SMA) ''' alias = ('RSI_Cutler',) @@ -224,9 +315,23 @@ class RSI_SMA(RSI): class RSI_EMA(RSI): ''' - Uses an ExponentialMovingAverage as described in Wikipedia + 使用 Wikipedia 中描述的 ExponentialMovingAverage 版本 RSI。 + + Args: + period: RSI 平滑周期。 + lookback: up/down day 比较的回看周期。 + + Returns: + RSI_EMA: 输出 ``rsi`` line 的 ExponentialMovingAverage 版 RSI indicator。 See: - http://en.wikipedia.org/wiki/Relative_strength_index + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(RSI_EMA) ''' params = (('movav', MovAv.Exponential),) diff --git a/backtrader/indicators/sma.py b/backtrader/indicators/sma.py index 0207131ee..5c6f8a13c 100644 --- a/backtrader/indicators/sma.py +++ b/backtrader/indicators/sma.py @@ -26,20 +26,32 @@ class MovingAverageSimple(MovingAverageBase): ''' - Non-weighted average of the last n periods + 最近 n 个周期的非加权平均值。 + + Args: + period: 平均周期。 + + Returns: + MovingAverageSimple: 输出 ``sma`` line 的 indicator。 Formula: - movav = Sum(data, period) / period See also: - http://en.wikipedia.org/wiki/Moving_average#Simple_moving_average + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(MovingAverageSimple, period=20) ''' alias = ('SMA', 'SimpleMovingAverage',) lines = ('sma',) def __init__(self): - # Before super to ensure mixins (right-hand side in subclassing) - # can see the assignment operation and operate on the line + # 放在 super 之前,确保 mixin(子类化时右侧基类)能看到赋值操作并处理该 line self.lines[0] = Average(self.data, period=self.p.period) super(MovingAverageSimple, self).__init__() diff --git a/backtrader/indicators/smma.py b/backtrader/indicators/smma.py index 6859f8498..9ed8c90b5 100644 --- a/backtrader/indicators/smma.py +++ b/backtrader/indicators/smma.py @@ -26,14 +26,20 @@ class SmoothedMovingAverage(MovingAverageBase): ''' - Smoothing Moving Average used by Wilder in his 1978 book `New Concepts in - Technical Trading` + Wilder 在 1978 年 *New Concepts in Technical Trading* 中使用的 + Smoothing Moving Average。 - Defined in his book originally as: + Args: + period: 平滑周期。 + + Returns: + SmoothedMovingAverage: 输出 ``smma`` line 的 indicator。 + + 书中的原始定义为: - new_value = (old_value * (period - 1) + new_data) / period - Can be expressed as a SmoothingMovingAverage with the following factors: + 可用下列因子表示为 ``SmoothingMovingAverage``: - self.smfactor -> 1.0 / period - self.smfactor1 -> `1.0 - self.smfactor` @@ -43,14 +49,20 @@ class SmoothedMovingAverage(MovingAverageBase): See also: - http://en.wikipedia.org/wiki/Moving_average#Modified_moving_average + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(SmoothedMovingAverage, period=20) ''' alias = ('SMMA', 'WilderMA', 'MovingAverageSmoothed', 'MovingAverageWilder', 'ModifiedMovingAverage',) lines = ('smma',) def __init__(self): - # Before super to ensure mixins (right-hand side in subclassing) - # can see the assignment operation and operate on the line + # 放在 super 之前,确保 mixin(子类化时右侧基类)能看到赋值操作并处理该 line self.lines[0] = ExponentialSmoothing( self.data, period=self.p.period, diff --git a/backtrader/indicators/stochastic.py b/backtrader/indicators/stochastic.py index 1cb5c7caa..b71568244 100644 --- a/backtrader/indicators/stochastic.py +++ b/backtrader/indicators/stochastic.py @@ -25,6 +25,7 @@ class _StochasticBase(Indicator): + '''Stochastic 的基类,用于统一 %K/%D 计算、参数和绘图参考线。''' lines = ('percK', 'percD',) params = (('period', 14), ('period_dfast', 3), ('movav', MovAv.Simple), ('upperband', 80.0), ('lowerband', 20.0), @@ -57,15 +58,25 @@ def __init__(self): class StochasticFast(_StochasticBase): ''' - By Dr. George Lane in the 50s. It compares a closing price to the price - range and tries to show convergence if the closing prices are close to the - extremes + Dr. George Lane 在 20 世纪 50 年代提出的 StochasticFast。它比较 close + 与价格区间的位置,当 close 靠近极值时尝试显示 convergence。 - - It will go up if closing prices are close to the highs - - It will roughly go down if closing prices are close to the lows + - close 接近 high 时通常上升 + - close 接近 low 时通常下降 - It shows divergence if the extremes keep on growing but closing prices - do not in the same manner (distance to the extremes grow) + 当极值继续扩张而 close 未同步接近极值时,可用于观察 divergence。 + + Args: + period: high/low 回看周期。 + period_dfast: %D 快线平滑周期。 + movav: 用于平滑的 Moving Average 类型。 + upperband: 绘图时的上轨参考线。 + lowerband: 绘图时的下轨参考线。 + safediv: 是否保护除零。 + safezero: 除零时使用的默认值。 + + Returns: + StochasticFast: 输出 ``percK`` 与 ``percD`` line 的 indicator。 Formula: - hh = highest(data.high, period) @@ -77,6 +88,13 @@ class StochasticFast(_StochasticBase): See: - http://en.wikipedia.org/wiki/Stochastic_oscillator + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(StochasticFast) ''' def __init__(self): super(StochasticFast, self).__init__() @@ -86,11 +104,19 @@ def __init__(self): class Stochastic(_StochasticBase): ''' - The regular (or slow version) adds an additional moving average layer and - thus: + 常规版(或 slow version)额外添加一层 Moving Average,因此: + + - StochasticFast 的 percD line 会成为 percK line + - percD 会成为原始 percD 上 ``period_dslow`` 周期的 Moving Average + + Args: + period: high/low 回看周期。 + period_dfast: 快速 %D 平滑周期。 + period_dslow: 慢速 %D 平滑周期。 + movav: 用于平滑的 Moving Average 类型。 - - The percD line of the StochasticFast becomes the percK line - - percD becomes a moving average of period_dslow of the original percD + Returns: + Stochastic: 输出 ``percK`` 与 ``percD`` line 的 indicator。 Formula: - k = k @@ -99,6 +125,13 @@ class Stochastic(_StochasticBase): See: - http://en.wikipedia.org/wiki/Stochastic_oscillator + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(Stochastic) ''' alias = ('StochasticSlow',) params = (('period_dslow', 3),) @@ -116,12 +149,22 @@ def __init__(self): class StochasticFull(_StochasticBase): ''' - This version displays the 3 possible lines: + 该版本显示 3 条可用 line: - percK - percD - percSlow + Args: + period: high/low 回看周期。 + period_dfast: 快速 %D 平滑周期。 + period_dslow: 慢速 %D 平滑周期。 + movav: 用于平滑的 Moving Average 类型。 + + Returns: + StochasticFull: 输出 ``percK``、``percD`` 与 ``percDSlow`` line 的 + indicator。 + Formula: - k = d - d = MovingAverage(k, period_dslow) @@ -129,6 +172,13 @@ class StochasticFull(_StochasticBase): See: - http://en.wikipedia.org/wiki/Stochastic_oscillator + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(StochasticFull) ''' lines = ('percDSlow',) params = (('period_dslow', 3),) diff --git a/backtrader/indicators/trix.py b/backtrader/indicators/trix.py index 03964a54e..c6de9c5c2 100644 --- a/backtrader/indicators/trix.py +++ b/backtrader/indicators/trix.py @@ -26,8 +26,16 @@ class Trix(Indicator): ''' - Defined by Jack Hutson in the 80s and shows the Rate of Change (%) or slope - of a triple exponentially smoothed moving average + Jack Hutson 在 20 世纪 80 年代定义,用于显示三重指数平滑 Moving Average 的 + Rate of Change (%) 或斜率。 + + Args: + period: EMA 平滑周期。 + _rocperiod: 计算 Rate of Change 的回看周期。 + _movav: 用于平滑的 Moving Average 类型。 + + Returns: + Trix: 输出 ``trix`` line 的 indicator。 Formula: - ema1 = EMA(data, period) @@ -35,14 +43,20 @@ class Trix(Indicator): - ema3 = EMA(ema2, period) - trix = 100 * (ema3 - ema3(-1)) / ema3(-1) - The final formula can be simplified to: 100 * (ema3 / ema3(-1) - 1) + 最终公式可简化为:100 * (ema3 / ema3(-1) - 1) - The moving average used is the one originally defined by Wilder, - the SmoothedMovingAverage + 默认使用 EMA 作为 Moving Average。 See: - https://en.wikipedia.org/wiki/Trix_(technical_analysis) - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:trix + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(Trix, period=15) ''' alias = ('TRIX',) lines = ('trix',) @@ -62,7 +76,7 @@ def __init__(self): ema2 = self.p._movav(ema1, period=self.p.period) ema3 = self.p._movav(ema2, period=self.p.period) - # 1 period Percentage Rate of Change + # 1 周期 Percentage Rate of Change self.lines.trix = 100.0 * (ema3 / ema3(-self.p._rocperiod) - 1.0) super(Trix, self).__init__() @@ -70,7 +84,16 @@ def __init__(self): class TrixSignal(Trix): ''' - Extension of Trix with a signal line (ala MACD) + Trix 的扩展版本,额外添加类似 MACD 的 signal line。 + + Args: + period: EMA 平滑周期。 + sigperiod: signal line 的平滑周期。 + _rocperiod: 计算 Rate of Change 的回看周期。 + _movav: 用于平滑的 Moving Average 类型。 + + Returns: + TrixSignal: 输出 ``trix`` 与 ``signal`` line 的 indicator。 Formula: - trix = Trix(data, period) @@ -78,6 +101,13 @@ class TrixSignal(Trix): See: - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:trix + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(TrixSignal, period=15, sigperiod=9) ''' lines = ('signal',) params = (('sigperiod', 9),) diff --git a/backtrader/indicators/tsi.py b/backtrader/indicators/tsi.py index 8b8025f23..c0d4c0c32 100644 --- a/backtrader/indicators/tsi.py +++ b/backtrader/indicators/tsi.py @@ -28,12 +28,19 @@ class TrueStrengthIndicator(bt.Indicator): ''' - The True Strength Indicators was first introduced in Stocks & Commodities - Magazine by its author William Blau. It measures momentum with a double - exponential (default) of the prices. + William Blau 在 *Stocks & Commodities* 杂志中提出的 True Strength Indicator。 + 它默认通过价格的 double exponential 平滑来衡量 momentum。 - It shows divergence if the extremes keep on growign but closing prices - do not in the same manner (distance to the extremes grow) + 当极值继续扩张而收盘价没有同步扩张时,它可用于观察 divergence。 + + Args: + period1: 第一次平滑周期。 + period2: 第二次平滑周期。 + pchange: 计算价格变化的回看周期。 + _movav: 用于平滑的 Moving Average 类型。 + + Returns: + TrueStrengthIndicator: 输出 ``tsi`` line 的 indicator。 Formula: - price_change = close - close(pchange periods ago) @@ -46,12 +53,12 @@ class TrueStrengthIndicator(bt.Indicator): See: - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:true_strength_index - Params + --- + 交互界面使用示范: - - ``period1``: the period for the 1st smoothing - - ``period2``: the period for the 2nd smoothing - - ``pchange``: the lookback period for the price change - - ``_movav``: the moving average to apply for the smoothing + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(TrueStrengthIndicator, period1=25, period2=13) ''' alias = ('TSI',) params = ( diff --git a/backtrader/indicators/ultimateoscillator.py b/backtrader/indicators/ultimateoscillator.py index cd9a88e6c..6fcb4948a 100644 --- a/backtrader/indicators/ultimateoscillator.py +++ b/backtrader/indicators/ultimateoscillator.py @@ -28,6 +28,19 @@ class UltimateOscillator(bt.Indicator): ''' + Ultimate Oscillator 用多个周期的 Buying Pressure 与 TrueRange 比值衡量 + momentum。 + + Args: + p1: 短周期求和窗口。 + p2: 中周期求和窗口。 + p3: 长周期求和窗口。 + upperband: 绘图时的上轨参考线。 + lowerband: 绘图时的下轨参考线。 + + Returns: + UltimateOscillator: 输出 ``uo`` line 的 indicator。 + Formula: # Buying Pressure = Close - TrueLow BP = Close - Minimum(Low or Prior Close) @@ -45,6 +58,13 @@ class UltimateOscillator(bt.Indicator): - https://en.wikipedia.org/wiki/Ultimate_oscillator - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:ultimate_oscillator + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(UltimateOscillator) ''' lines = ('uo',) @@ -60,9 +80,9 @@ def _plotinit(self): baseticks = [10.0, 50.0, 90.0] hlines = [self.p.upperband, self.p.lowerband] - # Plot lines at 0 & 100 to make the scale complete + upper/lower/bands + # 绘制上下轨参考线 self.plotinfo.plotyhlines = hlines - # Plot ticks at "baseticks" + the user specified upper/lower bands + # 绘制基础刻度与用户指定的上下轨刻度 self.plotinfo.plotyticks = baseticks + hlines def __init__(self): @@ -73,7 +93,7 @@ def __init__(self): av14 = SumN(bp, period=self.p.p2) / SumN(tr, period=self.p.p2) av28 = SumN(bp, period=self.p.p3) / SumN(tr, period=self.p.p3) - # Multiply/divide floats outside of formula to reduce line objects + # 将浮点乘除移到公式外,减少 line 对象数量 factor = 100.0 / (4.0 + 2.0 + 1.0) uo = (4.0 * factor) * av7 + (2.0 * factor) * av14 + factor * av28 self.lines.uo = uo diff --git a/backtrader/indicators/vortex.py b/backtrader/indicators/vortex.py index dd369cc40..607155e8b 100644 --- a/backtrader/indicators/vortex.py +++ b/backtrader/indicators/vortex.py @@ -26,9 +26,23 @@ class Vortex(bt.Indicator): ''' + Vortex Indicator,用于衡量正向和负向趋势运动。 + + Args: + period: 统计周期。 + + Returns: + Vortex: 输出 ``vi_plus`` 和 ``vi_minus`` line 的 indicator。 + See: - http://www.vortexindicator.com/VFX_VORTEX.PDF + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(Vortex, period=14) ''' lines = ('vi_plus', 'vi_minus',) diff --git a/backtrader/indicators/williams.py b/backtrader/indicators/williams.py index cafe0a97b..9e453153d 100644 --- a/backtrader/indicators/williams.py +++ b/backtrader/indicators/williams.py @@ -27,10 +27,17 @@ class WilliamsR(Indicator): ''' - Developed by Larry Williams to show the relation of closing prices to - the highest-lowest range of a given period. + Larry Williams 开发的 Williams %R,用于显示 close 与指定周期最高-最低区间的关系。 - Known as Williams %R (but % is not allowed in Python identifiers) + 该指标通常称为 Williams %R,但 Python 标识符中不能使用 ``%``。 + + Args: + period: 最高价与最低价的回看周期。 + upperband: 绘图时的上轨参考线。 + lowerband: 绘图时的下轨参考线。 + + Returns: + WilliamsR: 输出 ``percR`` line 的 indicator。 Formula: - num = highest_period - close @@ -39,6 +46,13 @@ class WilliamsR(Indicator): See: - http://en.wikipedia.org/wiki/Williams_%25R + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(WilliamsR, period=14) ''' lines = ('percR',) params = (('period', 14), @@ -63,17 +77,29 @@ def __init__(self): class WilliamsAD(Indicator): ''' - By Larry Williams. It does cumulatively measure if the price is - accumulating (upwards) or distributing (downwards) by using the concept of - UpDays and DownDays. + Larry Williams 提出的 Williams Accumulation/Distribution。它使用 UpDays 与 + DownDays 的概念,累计衡量价格是在 accumulation(向上)还是 + distribution(向下)。 - Prices can go upwards but do so in a fashion that no longer shows - accumulation because updays and downdays are canceling out each other, - creating a divergence. + 价格可能继续上涨,但若 upday 与 downday 相互抵消,accumulation 不再同步增强, + 就可能形成 divergence。 + + Args: + data: 含有 close/high/low line 的数据源。 + + Returns: + WilliamsAD: 输出 ``ad`` line 的 indicator。 See: - http://www.metastock.com/Customer/Resources/TAAZ/?p=125 - http://ta.mql4.com/indicators/trends/williams_accumulation_distribution + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(WilliamsAD) ''' lines = ('ad',) diff --git a/backtrader/indicators/wma.py b/backtrader/indicators/wma.py index 62fecc0a6..9ce150099 100644 --- a/backtrader/indicators/wma.py +++ b/backtrader/indicators/wma.py @@ -28,8 +28,13 @@ class WeightedMovingAverage(MovingAverageBase): ''' - A Moving Average which gives an arithmetic weighting to values with the - newest having the more weight + 对值做算术加权的 Moving Average,越新的值权重越高。 + + Args: + period: 加权平均周期。 + + Returns: + WeightedMovingAverage: 输出 ``wma`` line 的 indicator。 Formula: - weights = range(1, period + 1) @@ -38,6 +43,13 @@ class WeightedMovingAverage(MovingAverageBase): See also: - http://en.wikipedia.org/wiki/Moving_average#Weighted_moving_average + + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(WeightedMovingAverage, period=20) ''' alias = ('WMA', 'MovingAverageWeighted',) lines = ('wma',) @@ -46,8 +58,7 @@ def __init__(self): coef = 2.0 / (self.p.period * (self.p.period + 1.0)) weights = tuple(float(x) for x in range(1, self.p.period + 1)) - # Before super to ensure mixins (right-hand side in subclassing) - # can see the assignment operation and operate on the line + # 放在 super 之前,确保 mixin(子类化时右侧基类)能看到赋值操作并处理该 line self.lines[0] = AverageWeighted( self.data, period=self.p.period, coef=coef, weights=weights) diff --git a/backtrader/indicators/zlema.py b/backtrader/indicators/zlema.py index ef3c57182..f259b85fb 100644 --- a/backtrader/indicators/zlema.py +++ b/backtrader/indicators/zlema.py @@ -27,9 +27,16 @@ class ZeroLagExponentialMovingAverage(MovingAverageBase): ''' - The zero-lag exponential moving average (ZLEMA) is a variation of the EMA - which adds a momentum term aiming to reduce lag in the average so as to - track current prices more closely. + Zero-lag Exponential Moving Average(ZLEMA)是 EMA 的变体。 + + 它加入 momentum 项来降低平均值滞后,使其更贴近当前价格。 + + Args: + period: 平滑周期。 + _movav: 用于内部计算的 Moving Average 类型。 + + Returns: + ZeroLagExponentialMovingAverage: 输出 ``zlema`` line 的 indicator。 Formula: - lag = (period - 1) / 2 @@ -38,6 +45,12 @@ class ZeroLagExponentialMovingAverage(MovingAverageBase): See also: - http://user42.tuxfamily.org/chart/manual/Zero_002dLag-Exponential-Moving-Average.html + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(ZeroLagExponentialMovingAverage, period=20) ''' alias = ('ZLEMA', 'ZeroLagEma',) lines = ('zlema',) diff --git a/backtrader/indicators/zlind.py b/backtrader/indicators/zlind.py index 19ec77e10..0d4602bd1 100644 --- a/backtrader/indicators/zlind.py +++ b/backtrader/indicators/zlind.py @@ -30,29 +30,39 @@ class ZeroLagIndicator(MovingAverageBase): - '''By John Ehlers and Ric Way + '''John Ehlers 与 Ric Way 提出的 ZeroLagIndicator。 - The zero-lag indicator (ZLIndicator) is a variation of the EMA - which modifies the EMA by trying to minimize the error (distance price - - error correction) and thus reduce the lag + zero-lag indicator(ZLIndicator)是 EMA 的变体,通过最小化误差 + (price 与 error correction 的距离)来修正 EMA,从而降低滞后。 + + Args: + period: EMA 计算周期。 + gainlimit: 搜索 error correction factor 的增益上限。 + _movav: 用于计算基础 EMA 的 Moving Average 类型。 + + Returns: + ZeroLagIndicator: 输出 ``ec`` line 的 Moving Average indicator。 Formula: - EMA(data, period) - - For each iteration calculate a best-error-correction of the ema (see - the paper and/or the code) iterating over ``-bestgain`` -> - ``+bestgain`` for the error correction factor (both incl.) + - 每轮遍历 ``-bestgain`` -> ``+bestgain``(含两端),为 EMA 计算最佳 + error correction。 - - The default moving average is EMA, but can be changed with the - parameter ``_movav`` + - 默认 Moving Average 为 EMA,可通过参数 ``_movav`` 修改。 - .. note:: the passed moving average must calculate alpha (and 1 - - alpha) and make them available as attributes ``alpha`` and - ``alpha1`` in the instance + .. note:: 传入的 Moving Average 必须计算 alpha(以及 1 - alpha),并在 + 实例上以 ``alpha`` 和 ``alpha1`` 属性暴露。 See also: - http://www.mesasoftware.com/papers/ZeroLag.pdf + --- + 交互界面使用示范: + + >>> from backtrader import Cerebro + >>> cerebro = Cerebro() + >>> cerebro.addindicator(ZeroLagIndicator, period=30) ''' alias = ('ZLIndicator', 'ZLInd', 'EC', 'ErrorCorrecting',) lines = ('ec',) @@ -70,12 +80,12 @@ def __init__(self): self.ema = MovAv.EMA(period=self.p.period) self.limits = [-self.p.gainlimit, self.p.gainlimit + 1] - # To make mixins work - super at the end for cooperative inheritance + # 为了让 mixin 生效,将 super 放在末尾以支持协作式继承 super(ZeroLagIndicator, self).__init__() def next(self): - leasterror = MAXINT # 1000000 in original code - bestec = ema = self.ema[0] # seed value 1st time for ec + leasterror = MAXINT # 原始代码中为 1000000 + bestec = ema = self.ema[0] # ec 首次计算时的种子值 price = self.data[0] ec1 = self.lines.ec[-1] alpha, alpha1 = self.ema.alpha, self.ema.alpha1 diff --git a/backtrader/linebuffer.py b/backtrader/linebuffer.py index 0102ddbe0..70514d847 100644 --- a/backtrader/linebuffer.py +++ b/backtrader/linebuffer.py @@ -22,8 +22,7 @@ .. module:: linebuffer -Classes that hold the buffer for a *line* and can operate on it -with appends, forwarding, rewinding, resetting and other +保存 *line* buffer 的类,并提供 append、forward、rewind、reset 等操作。 .. moduleauthor:: Daniel Rodriguez @@ -49,25 +48,26 @@ class LineBuffer(LineSingle): ''' - LineBuffer defines an interface to an "array.array" (or list) in which - index 0 points to the item which is active for input and output. + 单条 line 的 buffer 类,用于保存当前值、历史值和必要的未来扩展值。 - Positive indices fetch values from the past (left hand side) - Negative indices fetch values from the future (if the array has been - extended on the right hand side) + 索引 ``0`` 始终指向当前输入/输出位置;正索引读取过去的值,负索引读取未来扩展值 + (如果右侧已经扩展)。 - With this behavior no index has to be passed around to entities which have - to work with the current value produced by other entities: the value is - always reachable at "0". + Args: + 无 - Likewise storing the current value produced by "self" is done at 0. + Returns: + LineBuffer: 可被 line 系统使用的单线 buffer。 - Additional operations to move the pointer (home, forward, extend, rewind, - advance getzero) are provided + --- + 交互界面使用示范: - The class can also hold "bindings" to other LineBuffers. When a value - is set in this class - it will also be set in the binding. + >>> line = LineBuffer() + >>> line.forward(value=10.0) + >>> line[0] + 10.0 + + 该类也可以绑定其他 ``LineBuffer``。当前 line 设置值时,会同步写入绑定 line。 ''' UnBounded, QBuffer = (0, 1) @@ -83,14 +83,10 @@ def get_idx(self): return self._idx def set_idx(self, idx, force=False): - # if QBuffer and the last position of the buffer was reached, keep - # it (unless force) as index 0. This allows resampling - # - forward adds a position, but the 1st one is discarded, the 0 is - # invariant - # force supports replaying, which needs the extra bar to float - # forward/backwards, because the last input is read, and after a - # "backwards" is used to update the previous data. Unless the position - # 0 was moved to the previous index, it would fail + # QBuffer 已到达 buffer 最后位置时,除非 force,否则保持它作为索引 0。 + # 这支持 resampling:forward 增加一个位置并丢弃第一个位置,但 0 保持不变。 + # force 用于 replaying;replaying 需要额外 bar 可以前后浮动,因为读到最后输入后, + # 会用 backwards 更新上一条数据。如果位置 0 没有移到前一个索引,就会失败。 if self.mode == self.QBuffer: if force or self._idx < self.lenmark: self._idx = idx @@ -100,14 +96,15 @@ def set_idx(self, idx, force=False): idx = property(get_idx, set_idx) def reset(self): - ''' Resets the internal buffer structure and the indices + '''重置内部 buffer 结构和索引。 + + Returns: + None ''' if self.mode == self.QBuffer: - # add extrasize to ensure resample/replay work because they will - # use backwards to erase the last bar/tick before delivering a new - # bar The previous forward would have discarded the bar "period" - # times ago and it will not come back. Having + 1 in the size - # allows the forward without removing that bar + # 添加 extrasize 以保证 resample/replay 可用。它们会用 backwards 擦除 + # 最后一个 bar/tick,再交付新的 bar。此前的 forward 可能已经丢弃了 period + # 之前的 bar,无法找回;额外 +1 可以在 forward 时保留该 bar。 self.array = collections.deque(maxlen=self.maxlen + self.extrasize) self.useislice = True else: @@ -129,15 +126,16 @@ def getindicators(self): return [] def minbuffer(self, size): - '''The linebuffer must guarantee the minimum requested size to be - available. + '''保证 linebuffer 至少拥有请求的尺寸。 + + Args: + size: 需要保证的最小 buffer 尺寸。 - In non-dqbuffer mode, this is always true (of course until data is - filled at the beginning, there are less values, but minperiod in the - framework should account for this. + Returns: + None - In dqbuffer mode the buffer has to be adjusted for this if currently - less than requested + 非 QBuffer 模式下该条件总是满足;QBuffer 模式下,如果当前尺寸不足,需要调整 + buffer。 ''' if self.mode != self.QBuffer or self.maxlen >= size: return @@ -150,12 +148,10 @@ def __len__(self): return self.lencount def buflen(self): - ''' Real data that can be currently held in the internal buffer + '''返回内部 buffer 当前可保存的真实数据量。 - The internal buffer can be longer than the actual stored data to - allow for "lookahead" operations. The real amount of data that is - held/can be held in the buffer - is returned + Returns: + int: 当前真实数据容量,不包含为 lookahead 保留的扩展区。 ''' return len(self.array) - self.extension @@ -163,18 +159,16 @@ def __getitem__(self, ago): return self.array[self.idx + ago] def get(self, ago=0, size=1): - ''' Returns a slice of the array relative to *ago* + '''返回相对 ``ago`` 的数组切片。 - Keyword Args: - ago (int): Point of the array to which size will be added - to return the slice size(int): size of the slice to return, - can be positive or negative - - If size is positive *ago* will mark the end of the iterable and vice - versa if size is negative + Args: + ago: 切片参考位置。 + size: 返回切片的长度,可为正数或负数。 Returns: - A slice of the underlying buffer + list | array.array: 底层 buffer 的切片。 + + ``size`` 为正数时,``ago`` 表示 iterable 的结束位置;为负数时含义相反。 ''' if self.useislice: start = self.idx + ago - size + 1 @@ -184,27 +178,25 @@ def get(self, ago=0, size=1): return self.array[self.idx + ago - size + 1:self.idx + ago + 1] def getzeroval(self, idx=0): - ''' Returns a single value of the array relative to the real zero - of the buffer + '''返回相对 buffer 真实零点的单个值。 - Keyword Args: - idx (int): Where to start relative to the real start of the buffer - size(int): size of the slice to return + Args: + idx: 相对真实起点的位置。 Returns: - A slice of the underlying buffer + float: 底层 buffer 中的单个值。 ''' return self.array[idx] def getzero(self, idx=0, size=1): - ''' Returns a slice of the array relative to the real zero of the buffer + '''返回相对 buffer 真实零点的切片。 - Keyword Args: - idx (int): Where to start relative to the real start of the buffer - size(int): size of the slice to return + Args: + idx: 相对真实起点的位置。 + size: 返回切片的长度。 Returns: - A slice of the underlying buffer + list | array.array: 底层 buffer 的切片。 ''' if self.useislice: return list(islice(self.array, idx, idx + size)) @@ -212,44 +204,53 @@ def getzero(self, idx=0, size=1): return self.array[idx:idx + size] def __setitem__(self, ago, value): - ''' Sets a value at position "ago" and executes any associated bindings + '''在 ``ago`` 位置设置值,并执行关联 binding。 - Keyword Args: - ago (int): Point of the array to which size will be added to return - the slice - value (variable): value to be set + Args: + ago: 要写入的相对位置。 + value: 要设置的值。 + + Returns: + None ''' self.array[self.idx + ago] = value for binding in self.bindings: binding[ago] = value def set(self, value, ago=0): - ''' Sets a value at position "ago" and executes any associated bindings + '''在 ``ago`` 位置设置值,并执行关联 binding。 - Keyword Args: - value (variable): value to be set - ago (int): Point of the array to which size will be added to return - the slice + Args: + value: 要设置的值。 + ago: 要写入的相对位置。 + + Returns: + None ''' self.array[self.idx + ago] = value for binding in self.bindings: binding[ago] = value def home(self): - ''' Rewinds the logical index to the beginning + '''把逻辑索引倒回开始位置。 - The underlying buffer remains untouched and the actual len can be found - out with buflen + Returns: + None + + 底层 buffer 不会被修改,实际长度可通过 ``buflen`` 查询。 ''' self.idx = -1 self.lencount = 0 def forward(self, value=NAN, size=1): - ''' Moves the logical index foward and enlarges the buffer as much as needed + '''向前移动逻辑索引,并按需扩展 buffer。 - Keyword Args: - value (variable): value to be set in new positins - size (int): How many extra positions to enlarge the buffer + Args: + value: 新位置填入的值。 + size: 要新增的位置数量。 + + Returns: + None ''' self.idx += size self.lencount += size @@ -258,13 +259,16 @@ def forward(self, value=NAN, size=1): self.array.append(value) def backwards(self, size=1, force=False): - ''' Moves the logical index backwards and reduces the buffer as much as needed + '''向后移动逻辑索引,并按需缩小 buffer。 - Keyword Args: - size (int): How many extra positions to rewind and reduce the - buffer + Args: + size: 要回退并缩减的位置数量。 + force: 是否强制移动 QBuffer 的索引。 + + Returns: + None ''' - # Go directly to property setter to support force + # 直接调用属性 setter,以支持 force self.set_idx(self._idx - size, force=force) self.lencount -= size for i in range(size): @@ -275,53 +279,57 @@ def rewind(self, size=1): self.lencount -= size def advance(self, size=1): - ''' Advances the logical index without touching the underlying buffer + '''只前进逻辑索引,不修改底层 buffer。 - Keyword Args: - size (int): How many extra positions to move forward + Args: + size: 要前进的位置数量。 + + Returns: + None ''' self.idx += size self.lencount += size def extend(self, value=NAN, size=0): - ''' Extends the underlying array with positions that the index will not reach + '''扩展底层数组,新增当前索引不会到达的位置。 - Keyword Args: - value (variable): value to be set in new positins - size (int): How many extra positions to enlarge the buffer + Args: + value: 新位置填入的值。 + size: 要新增的位置数量。 - The purpose is to allow for lookahead operations or to be able to - set values in the buffer "future" + Returns: + None + + 主要用于 lookahead,或向 buffer 的“未来”位置设置值。 ''' self.extension += size for i in range(size): self.array.append(value) def addbinding(self, binding): - ''' Adds another line binding + '''添加另一个 line binding。 - Keyword Args: - binding (LineBuffer): another line that must be set when this line - becomes a value + Args: + binding: 当前 line 设置值时也要同步设置的另一个 ``LineBuffer``。 + + Returns: + None ''' self.bindings.append(binding) - # record in the binding when the period is starting (never sooner - # than self) + # 在 binding 中记录 period 开始位置(永远不早于当前对象) binding.updateminperiod(self._minperiod) def plot(self, idx=0, size=None): - ''' Returns a slice of the array relative to the real zero of the buffer + '''返回相对 buffer 真实零点的绘图切片。 - Keyword Args: - idx (int): Where to start relative to the real start of the buffer - size(int): size of the slice to return - - This is a variant of getzero which unless told otherwise returns the - entire buffer, which is usually the idea behind plottint (all must - plotted) + Args: + idx: 相对真实起点的位置。 + size: 返回切片的长度;为空时返回整个 buffer。 Returns: - A slice of the underlying buffer + list | array.array: 底层 buffer 的切片。 + + 这是 ``getzero`` 的变体,默认返回整个 buffer,符合绘图时“全部都要画”的语义。 ''' return self.getzero(idx, size or len(self)) @@ -333,7 +341,10 @@ def plotrange(self, start, end): def oncebinding(self): ''' - Executes the bindings when running in "once" mode + 在 ``once`` 模式下执行 binding。 + + Returns: + None ''' larray = self.array blen = self.buflen() @@ -342,7 +353,13 @@ def oncebinding(self): def bind2lines(self, binding=0): ''' - Stores a binding to another line. "binding" can be an index or a name + 保存到另一条 line 的 binding。 + + Args: + binding: line 索引或 line 名称。 + + Returns: + LineBuffer: 当前对象自身。 ''' if isinstance(binding, string_types): line = getattr(self._owner.lines, binding) @@ -356,16 +373,14 @@ def bind2lines(self, binding=0): bind2line = bind2lines def __call__(self, ago=None): - '''Returns either a delayed verison of itself in the form of a - LineDelay object or a timeframe adapting version with regards to a ago - - Param: ago (default: None) + '''返回延迟版本或 timeframe 适配版本。 - If ago is None or an instance of LineRoot (a lines object) the - returned valued is a LineCoupler instance + Args: + ago: ``None`` 或 ``LineRoot`` 时返回 ``LineCoupler``;其他情况按整数处理, + 返回 ``LineDelay``。 - If ago is anything else, it is assumed to be an int and a LineDelay - object will be returned + Returns: + LineCoupler | LineDelay: 适配或延迟后的 line 对象。 ''' from .lineiterator import LineCoupler if ago is None or isinstance(ago, LineRoot): @@ -397,35 +412,57 @@ def time(self, ago=0, tz=None, naive=True): def dt(self, ago=0): ''' - return numeric date part of datetimefloat + 返回 datetime float 的数字日期部分。 + + Args: + ago: 相对当前位置。 + + Returns: + int: 日期部分。 ''' return math.trunc(self.array[self.idx + ago]) def tm_raw(self, ago=0): ''' - return raw numeric time part of datetimefloat + 返回 datetime float 的原始数字时间部分。 + + Args: + ago: 相对当前位置。 + + Returns: + float: 未转换的时间小数部分。 ''' - # This function is named raw because it retrieves the fractional part - # without transforming it to time to avoid the influence of the day - # count (integer part of coding) + # 命名为 raw,是因为它直接取小数部分,不转换为 time,以避免受日期计数 + # (编码整数部分)影响 return math.modf(self.array[self.idx + ago])[0] def tm(self, ago=0): ''' - return numeric time part of datetimefloat + 返回 datetime float 的数字时间部分。 + + Args: + ago: 相对当前位置。 + + Returns: + float: 转换后的时间部分。 ''' - # To avoid precision errors, this returns the fractional part after - # having converted it to a datetime.time object to avoid precision - # errors in comparisons + # 为避免精度误差,先转换为 datetime.time 对象,再返回小数时间部分, + # 用于后续比较 return time2num(num2date(self.array[self.idx + ago]).time()) def tm_lt(self, other, ago=0): ''' - return numeric time part of datetimefloat + 比较当前 datetime float 的时间部分是否小于 ``other``。 + + Args: + other: 要比较的数字时间部分。 + ago: 相对当前位置。 + + Returns: + bool: 比较结果。 ''' - # To compare a raw "tm" part (fractional part of coded datetime) - # with the tm of the current datetime, the raw "tm" has to be - # brought in sync with the current "day" count (integer part) to avoid + # 比较原始 tm(编码 datetime 的小数部分)和当前 datetime 的 tm 时, + # 需要把原始 tm 同步到当前 day count(整数部分)上 dtime = self.array[self.idx + ago] tm, dt = math.modf(dtime) @@ -433,11 +470,16 @@ def tm_lt(self, other, ago=0): def tm_le(self, other, ago=0): ''' - return numeric time part of datetimefloat + 比较当前 datetime float 的时间部分是否小于等于 ``other``。 + + Args: + other: 要比较的数字时间部分。 + ago: 相对当前位置。 + + Returns: + bool: 比较结果。 ''' - # To compare a raw "tm" part (fractional part of coded datetime) - # with the tm of the current datetime, the raw "tm" has to be - # brought in sync with the current "day" count (integer part) to avoid + # 比较原始 tm 和当前 datetime 的 tm 时,需要同步到当前 day count 上 dtime = self.array[self.idx + ago] tm, dt = math.modf(dtime) @@ -445,11 +487,16 @@ def tm_le(self, other, ago=0): def tm_eq(self, other, ago=0): ''' - return numeric time part of datetimefloat + 比较当前 datetime float 的时间部分是否等于 ``other``。 + + Args: + other: 要比较的数字时间部分。 + ago: 相对当前位置。 + + Returns: + bool: 比较结果。 ''' - # To compare a raw "tm" part (fractional part of coded datetime) - # with the tm of the current datetime, the raw "tm" has to be - # brought in sync with the current "day" count (integer part) to avoid + # 比较原始 tm 和当前 datetime 的 tm 时,需要同步到当前 day count 上 dtime = self.array[self.idx + ago] tm, dt = math.modf(dtime) @@ -457,11 +504,16 @@ def tm_eq(self, other, ago=0): def tm_gt(self, other, ago=0): ''' - return numeric time part of datetimefloat + 比较当前 datetime float 的时间部分是否大于 ``other``。 + + Args: + other: 要比较的数字时间部分。 + ago: 相对当前位置。 + + Returns: + bool: 比较结果。 ''' - # To compare a raw "tm" part (fractional part of coded datetime) - # with the tm of the current datetime, the raw "tm" has to be - # brought in sync with the current "day" count (integer part) to avoid + # 比较原始 tm 和当前 datetime 的 tm 时,需要同步到当前 day count 上 dtime = self.array[self.idx + ago] tm, dt = math.modf(dtime) @@ -469,11 +521,16 @@ def tm_gt(self, other, ago=0): def tm_ge(self, other, ago=0): ''' - return numeric time part of datetimefloat + 比较当前 datetime float 的时间部分是否大于等于 ``other``。 + + Args: + other: 要比较的数字时间部分。 + ago: 相对当前位置。 + + Returns: + bool: 比较结果。 ''' - # To compare a raw "tm" part (fractional part of coded datetime) - # with the tm of the current datetime, the raw "tm" has to be - # brought in sync with the current "day" count (integer part) to avoid + # 比较原始 tm 和当前 datetime 的 tm 时,需要同步到当前 day count 上 dtime = self.array[self.idx + ago] tm, dt = math.modf(dtime) @@ -481,30 +538,41 @@ def tm_ge(self, other, ago=0): def tm2dtime(self, tm, ago=0): ''' - Returns the given ``tm`` in the frame of the (ago bars) datatime. + 把给定 ``tm`` 转换到 ``ago`` bar 所在 datetime 的日期框架中。 + + Args: + tm: 数字时间部分。 + ago: 相对当前位置。 + + Returns: + float: 可比较的 datetime float。 - Useful for external comparisons to avoid precision errors + 该方法适合外部比较,可避免精度误差。 ''' return int(self.array[self.idx + ago]) + tm def tm2datetime(self, tm, ago=0): ''' - Returns the given ``tm`` in the frame of the (ago bars) datatime. + 把给定 ``tm`` 转换到 ``ago`` bar 所在日期上的 ``datetime``。 - Useful for external comparisons to avoid precision errors + Args: + tm: 数字时间部分。 + ago: 相对当前位置。 + + Returns: + datetime.datetime: 转换后的 datetime。 + + 该方法适合外部比较,可避免精度误差。 ''' return num2date(int(self.array[self.idx + ago]) + tm) class MetaLineActions(LineBuffer.__class__): ''' - Metaclass for Lineactions - - Scans the instance before init for LineBuffer (or parentclass LineSingle) - instances to calculate the minperiod for this instance + ``LineActions`` 的 metaclass,用于在 init 前扫描 line 并计算 minperiod。 - postinit it registers the instance to the owner (remember that owner has - been found in the base Metaclass for LineRoot) + postinit 阶段会把实例注册到 owner;owner 已经由 ``LineRoot`` 基类的 metaclass + 找到。 ''' _acache = dict() _acacheuse = False @@ -521,14 +589,14 @@ def __call__(cls, *args, **kwargs): if not cls._acacheuse: return super(MetaLineActions, cls).__call__(*args, **kwargs) - # implement a cache to avoid duplicating lines actions - ckey = (cls, tuple(args), tuple(kwargs.items())) # tuples hashable + # 实现缓存,避免重复创建 line action + ckey = (cls, tuple(args), tuple(kwargs.items())) # tuple 可 hash try: return cls._acache[ckey] - except TypeError: # something not hashable + except TypeError: # 存在不可 hash 的对象 return super(MetaLineActions, cls).__call__(*args, **kwargs) except KeyError: - pass # hashable but not in the cache + pass # 可 hash,但不在缓存中 _obj = super(MetaLineActions, cls).__call__(*args, **kwargs) return cls._acache.setdefault(ckey, _obj) @@ -537,15 +605,15 @@ def dopreinit(cls, _obj, *args, **kwargs): _obj, args, kwargs = \ super(MetaLineActions, cls).dopreinit(_obj, *args, **kwargs) - _obj._clock = _obj._owner # default setting + _obj._clock = _obj._owner # 默认设置 if isinstance(args[0], LineRoot): _obj._clock = args[0] - # Keep a reference to the datas for buffer adjustment purposes + # 保存 data 引用,供后续调整 buffer 使用 _obj._datas = [x for x in args if isinstance(x, LineRoot)] - # Do not produce anything until the operation lines produce something + # operation line 自身产出前,不产出任何值 _minperiods = [x._minperiod for x in args if isinstance(x, LineSingle)] mlines = [x.lines[0] for x in args if isinstance(x, LineMultiple)] @@ -553,7 +621,7 @@ def dopreinit(cls, _obj, *args, **kwargs): _minperiod = max(_minperiods or [1]) - # update own minperiod if needed + # 按需更新自身 minperiod _obj.updateminperiod(_minperiod) return _obj, args, kwargs @@ -562,7 +630,7 @@ def dopostinit(cls, _obj, *args, **kwargs): _obj, args, kwargs = \ super(MetaLineActions, cls).dopostinit(_obj, *args, **kwargs) - # register with _owner to be kicked later + # 注册到 _owner,后续由 owner 驱动 _obj._owner.addindicator(_obj) return _obj, args, kwargs @@ -582,11 +650,11 @@ def array(self): class LineActions(with_metaclass(MetaLineActions, LineBuffer)): ''' - Base class derived from LineBuffer intented to defined the - minimum interface to make it compatible with a LineIterator by - providing operational _next and _once interfaces. + 派生自 ``LineBuffer`` 的 line action 基类,用于提供与 ``LineIterator`` 兼容的 + 最小接口。 - The metaclass does the dirty job of calculating minperiods and registering + 该类提供可执行的 ``_next`` 和 ``_once`` 接口;metaclass 负责计算 minperiod 和注册 + 到 owner。 ''' _ltype = LineBuffer.IndType @@ -603,7 +671,7 @@ def qbuffer(self, savemem=0): def arrayize(obj): if isinstance(obj, LineRoot): if not isinstance(obj, LineSingle): - obj = obj.lines[0] # get 1st line from multiline + obj = obj.lines[0] # 从 multiline 中取第 1 条 line else: obj = PseudoArray(obj) @@ -617,7 +685,7 @@ def _next(self): if clock_len > self._minperiod: self.next() elif clock_len == self._minperiod: - # only called for the 1st value + # 只在第 1 个完整值时调用 self.nextstart() else: self.prenext() @@ -646,24 +714,22 @@ def LineNum(num): class _LineDelay(LineActions): ''' - Takes a LineBuffer (or derived) object and stores the value from - "ago" periods effectively delaying the delivery of data + line 延迟类,用于从 ``ago`` 个周期前取值,从而延迟数据交付。 ''' def __init__(self, a, ago): super(_LineDelay, self).__init__() self.a = a self.ago = ago - # Need to add the delay to the period. "ago" is 0 based and therefore - # we need to pass and extra 1 which is the minimum defined period for - # any data (which will be substracted inside addminperiod) + # 需要把 delay 加到 period 中。ago 从 0 开始,因此要额外传 1;这是任何 data + # 定义的最小 period,并会在 addminperiod 中被扣除 self.addminperiod(abs(ago) + 1) def next(self): self[0] = self.a[self.ago] def once(self, start, end): - # cache python dictionary lookups + # 缓存 Python 字典查找 dst = self.array src = self.a.array ago = self.ago @@ -674,17 +740,15 @@ def once(self, start, end): class _LineForward(LineActions): ''' - Takes a LineBuffer (or derived) object and stores the value from - "ago" periods from the future + line 前向类,用于把未来 ``ago`` 个周期的值写入当前延迟结构。 ''' def __init__(self, a, ago): super(_LineForward, self).__init__() self.a = a self.ago = ago - # Need to add the delay to the period. "ago" is 0 based and therefore - # we need to pass and extra 1 which is the minimum defined period for - # any data (which will be substracted inside addminperiod) + # 需要把 delay 加到 period 中。ago 从 0 开始,因此要额外传 1;这是任何 data + # 定义的最小 period,并会在 addminperiod 中被扣除 # self.addminperiod(abs(ago) + 1) if ago > self.a._minperiod: self.addminperiod(ago - self.a._minperiod + 1) @@ -693,7 +757,7 @@ def next(self): self[-self.ago] = self.a[0] def once(self, start, end): - # cache python dictionary lookups + # 缓存 Python 字典查找 dst = self.array src = self.a.array ago = self.ago @@ -705,30 +769,26 @@ def once(self, start, end): class LinesOperation(LineActions): ''' - Holds an operation that operates on a two operands. Example: mul + 双操作数 line 运算类,用于保存并执行类似 ``mul`` 的运算。 - It will "next"/traverse the array applying the operation on the - two operands and storing the result in self. + 每次 ``next`` 或遍历数组时,会把运算应用到两个操作数,并把结果存入自身。 - To optimize the operations and avoid conditional checks the right - next/once is chosen using the operation direction (normal or reversed) - and the nature of the operands (LineBuffer vs non-LineBuffer) + 为了优化执行并减少条件判断,会根据运算方向(普通或反向)和操作数性质 + (``LineBuffer`` 或非 ``LineBuffer``)选择对应的 ``next`` / ``once`` 实现。 - In the "once" operations "map" could be used as in: + ``once`` 运算中本可以使用 ``map``,例如:: operated = map(self.operation, srca[start:end], srcb[start:end]) self.array[start:end] = array.array(str(self.typecode), operated) - No real execution time benefits were appreciated and therefore the loops - have been kept in place for clarity (although the maps are not really - unclear here) + 实测没有明显执行收益,因此保留显式循环以增强可读性。 ''' def __init__(self, a, b, operation, r=False): super(LinesOperation, self).__init__() self.operation = operation - self.a = a # always a linebuffer + self.a = a # 始终是 linebuffer self.b = b self.r = r @@ -762,7 +822,7 @@ def once(self, start, end): self._once_val_op_r(start, end) def _once_op(self, start, end): - # cache python dictionary lookups + # 缓存 Python 字典查找 dst = self.array srca = self.a.array srcb = self.b.array @@ -772,7 +832,7 @@ def _once_op(self, start, end): dst[i] = op(srca[i], srcb[i]) def _once_time_op(self, start, end): - # cache python dictionary lookups + # 缓存 Python 字典查找 dst = self.array srca = self.a.array srcb = self.b @@ -783,7 +843,7 @@ def _once_time_op(self, start, end): dst[i] = op(num2date(srca[i], tz=tz).time(), srcb) def _once_val_op(self, start, end): - # cache python dictionary lookups + # 缓存 Python 字典查找 dst = self.array srca = self.a.array srcb = self.b @@ -793,7 +853,7 @@ def _once_val_op(self, start, end): dst[i] = op(srca[i], srcb) def _once_val_op_r(self, start, end): - # cache python dictionary lookups + # 缓存 Python 字典查找 dst = self.array srca = self.a srcb = self.b.array @@ -805,10 +865,9 @@ def _once_val_op_r(self, start, end): class LineOwnOperation(LineActions): ''' - Holds an operation that operates on a single operand. Example: abs + 单操作数 line 运算类,用于保存并执行类似 ``abs`` 的运算。 - It will "next"/traverse the array applying the operation and storing - the result in self + 每次 ``next`` 或遍历数组时,会应用运算并把结果存入自身。 ''' def __init__(self, a, operation): super(LineOwnOperation, self).__init__() @@ -820,7 +879,7 @@ def next(self): self[0] = self.operation(self.a[0]) def once(self, start, end): - # cache python dictionary lookups + # 缓存 Python 字典查找 dst = self.array srca = self.a.array op = self.operation diff --git a/backtrader/lineiterator.py b/backtrader/lineiterator.py index 2ead478b8..bc74984d3 100644 --- a/backtrader/lineiterator.py +++ b/backtrader/lineiterator.py @@ -40,12 +40,11 @@ def donew(cls, *args, **kwargs): _obj, args, kwargs = \ super(MetaLineIterator, cls).donew(*args, **kwargs) - # Prepare to hold children that need to be calculated and - # influence minperiod - Moved here to support LineNum below + # 准备用于保存需要计算、且会影响 minperiod 的子对象 + # 放在这里是为了支持下面的 LineNum _obj._lineiterators = collections.defaultdict(list) - # Scan args for datas ... if none are found, - # use the _owner (to have a clock) + # 扫描 args 中的 data;如果没有找到,则使用 _owner 作为 clock mindatas = _obj._mindatas lastarg = 0 _obj.datas = [] @@ -54,12 +53,12 @@ def donew(cls, *args, **kwargs): _obj.datas.append(LineSeriesMaker(arg)) elif not mindatas: - break # found not data and must not be collected + break # 找到非 data,且不需要继续收集 else: try: _obj.datas.append(LineSeriesMaker(LineNum(arg))) except: - # Not a LineNum and is not a LineSeries - bail out + # 既不是 LineNum,也不是 LineSeries,退出扫描 break mindatas = max(0, mindatas - 1) @@ -67,18 +66,15 @@ def donew(cls, *args, **kwargs): newargs = args[lastarg:] - # If no datas have been passed to an indicator ... use the - # main datas of the owner, easing up adding "self.data" ... + # 如果 indicator 没有传入 data,则使用 owner 的主 data,方便直接写 self.data if not _obj.datas and isinstance(_obj, (IndicatorBase, ObserverBase)): _obj.datas = _obj._owner.datas[0:mindatas] - # Create a dictionary to be able to check for presence - # lists in python use "==" operator when testing for presence with "in" - # which doesn't really check for presence but for equality + # 创建字典以便检查对象是否存在。Python list 的 in 会使用 "==" 判断, + # 实际检查的是相等而不是对象存在性。 _obj.ddatas = {x: None for x in _obj.datas} - # For each found data add access member - - # for the first data 2 (data and data0) + # 为每个找到的 data 添加访问成员;第一个 data 同时拥有 data 和 data0 if _obj.datas: _obj.data = data = _obj.datas[0] @@ -97,7 +93,7 @@ def donew(cls, *args, **kwargs): setattr(_obj, 'data%d_%s' % (d, linealias), line) setattr(_obj, 'data%d_%d' % (d, l), line) - # Parameter values have now been set before __init__ + # 参数值此时已经在 __init__ 前设置完毕 _obj.dnames = DotDict([(d._name, d) for d in _obj.datas if getattr(d, '_name', '')]) @@ -107,21 +103,18 @@ def dopreinit(cls, _obj, *args, **kwargs): _obj, args, kwargs = \ super(MetaLineIterator, cls).dopreinit(_obj, *args, **kwargs) - # if no datas were found use, use the _owner (to have a clock) + # 如果没有找到 data,则使用 _owner 作为 clock _obj.datas = _obj.datas or [_obj._owner] - # 1st data source is our ticking clock + # 第一个 data source 是当前对象的 ticking clock _obj._clock = _obj.datas[0] - # To automatically set the period Start by scanning the found datas - # No calculation can take place until all datas have yielded "data" - # A data could be an indicator and it could take x bars until - # something is produced + # 通过扫描找到的 data 自动设置 period 起点。所有 data 都产出值之前不能计算。 + # data 本身也可能是 indicator,并且可能需要若干 bar 才能产出值。 _obj._minperiod = \ max([x._minperiod for x in _obj.datas] or [_obj._minperiod]) - # The lines carry at least the same minperiod as - # that provided by the datas + # line 至少要携带与 data 相同的 minperiod for line in _obj.lines: line.addminperiod(_obj._minperiod) @@ -131,14 +124,13 @@ def dopostinit(cls, _obj, *args, **kwargs): _obj, args, kwargs = \ super(MetaLineIterator, cls).dopostinit(_obj, *args, **kwargs) - # my minperiod is as large as the minperiod of my lines + # 自身 minperiod 至少等于所有 line 的最大 minperiod _obj._minperiod = max([x._minperiod for x in _obj.lines]) - # Recalc the period + # 重新计算 period _obj._periodrecalc() - # Register (my)self as indicator to owner once - # _minperiod has been calculated + # _minperiod 计算完成后,把自身注册到 owner if _obj._owner is not None: _obj._owner.addindicator(_obj) @@ -146,7 +138,9 @@ def dopostinit(cls, _obj, *args, **kwargs): class LineIterator(with_metaclass(MetaLineIterator, LineSeries)): - _nextforce = False # force cerebro to run in next mode (runonce=False) + '''line iterator 的基类,用于调度 data、indicator、observer 的 next/once 生命周期。''' + + _nextforce = False # 强制 cerebro 使用 next 模式(runonce=False) _mindatas = 1 _ltype = LineSeries.IndType @@ -167,9 +161,8 @@ class LineIterator(with_metaclass(MetaLineIterator, LineSeries)): plotmaster=None,) def _periodrecalc(self): - # last check in case not all lineiterators were assigned to - # lines (directly or indirectly after some operations) - # An example is Kaufman's Adaptive Moving Average + # 最后检查一次,防止部分 lineiterator 没有被直接或间接分配到 line + # 典型例子是 Kaufman's Adaptive Moving Average indicators = self._lineiterators[LineIterator.IndType] indperiods = [ind._minperiod for ind in indicators] indminperiod = max(indperiods or [self._minperiod]) @@ -206,19 +199,19 @@ def getobservers(self): return self._lineiterators[LineIterator.ObsType] def addindicator(self, indicator): - # store in right queue + # 存入对应队列 self._lineiterators[indicator._ltype].append(indicator) - # use getattr because line buffers don't have this attribute + # 使用 getattr,因为 line buffer 没有该属性 if getattr(indicator, '_nextforce', False): - # the indicator needs runonce=False + # 该 indicator 需要 runonce=False o = self while o is not None: if o._ltype == LineIterator.StratType: o.cerebro._disable_runonce() break - o = o._owner # move up the hierarchy + o = o._owner # 沿层级向上移动 def bindlines(self, owner=None, own=None): if not owner: @@ -252,7 +245,7 @@ def bindlines(self, owner=None, own=None): return self - # Alias which may be more readable + # 可读性更好的别名 bind2lines = bindlines bind2line = bind2lines @@ -265,21 +258,20 @@ def _next(self): self._notify() if self._ltype == LineIterator.StratType: - # supporting datas with different lengths + # 支持不同长度的 data minperstatus = self._getminperstatus() if minperstatus < 0: self.next() elif minperstatus == 0: - self.nextstart() # only called for the 1st value + self.nextstart() # 只在第 1 个完整值时调用 else: self.prenext() else: - # assume indicators and others operate on same length datas - # although the above operation can be generalized + # 假定 indicator 等对象运行在同长度 data 上;上面的逻辑也可以泛化到这里 if clock_len > self._minperiod: self.next() elif clock_len == self._minperiod: - self.nextstart() # only called for the 1st value + self.nextstart() # 只在第 1 个完整值时调用 elif clock_len: self.prenext() @@ -310,9 +302,8 @@ def _once(self): self.home() - # These 3 remain empty for a strategy and therefore play no role - # because a strategy will always be executed on a next basis - # indicators are each called with its min period + # 对 strategy 而言这 3 个方法保持为空,因此不起作用;strategy 总是按 next 执行。 + # indicator 则会按各自 minperiod 调用。 self.preonce(0, self._minperiod - 1) self.oncestart(self._minperiod - 1, self._minperiod) self.once(self._minperiod, self.buflen()) @@ -331,25 +322,32 @@ def once(self, start, end): def prenext(self): ''' - This method will be called before the minimum period of all - datas/indicators have been meet for the strategy to start executing + 在所有 data/indicator 都满足最小 period 前调用。 + + Returns: + None ''' pass def nextstart(self): ''' - This method will be called once, exactly when the minimum period for - all datas/indicators have been meet. The default behavior is to call - next + 在所有 data/indicator 刚好满足最小 period 时调用一次。 + + Returns: + None + + 默认行为是调用 ``next``。 ''' - # Called once for 1st full calculation - defaults to regular next + # 第一次完整计算时调用一次,默认转到普通 next self.next() def next(self): ''' - This method will be called for all remaining data points when the - minimum period for all datas/indicators have been meet. + 所有 data/indicator 满足最小 period 后,对剩余数据点调用。 + + Returns: + None ''' pass @@ -367,20 +365,21 @@ def qbuffer(self, savemem=0): for line in self.lines: line.qbuffer() - # If called, anything under it, must save + # 如果调用到这里,下层对象都必须节省内存 for obj in self._lineiterators[self.IndType]: obj.qbuffer(savemem=1) - # Tell datas to adjust buffer to minimum period + # 通知 data 按最小 period 调整 buffer for data in self.datas: data.minbuffer(self._minperiod) -# This 3 subclasses can be used for identification purposes within LineIterator -# or even outside (like in LineObservers) -# for the 3 subbranches without generating circular import references +# 这 3 个子类用于在 LineIterator 内部或外部(例如 LineObservers)识别 3 个分支, +# 同时避免产生循环 import class DataAccessor(LineIterator): + '''数据访问基类,用于统一暴露 price line 的枚举别名。''' + PriceClose = DataSeries.Close PriceLow = DataSeries.Low PriceHigh = DataSeries.High @@ -391,21 +390,29 @@ class DataAccessor(LineIterator): class IndicatorBase(DataAccessor): + '''indicator 的基类,用于标识 indicator 分支。''' + pass class ObserverBase(DataAccessor): + '''observer 的基类,用于标识 observer 分支。''' + pass class StrategyBase(DataAccessor): + '''strategy 的基类,用于标识 strategy 分支。''' + pass -# Utility class to couple lines/lineiterators which may have different lengths -# Will only work when runonce=False is passed to Cerebro +# 用于耦合不同长度 line/lineiterator 的工具类 +# 只有向 Cerebro 传入 runonce=False 时才可用 class SingleCoupler(LineActions): + '''单线 coupler,用于在不同长度的 line 之间保持最近一个可用值。''' + def __init__(self, cdata, clock=None): super(SingleCoupler, self).__init__() self._clock = clock if clock is not None else self._owner @@ -423,12 +430,14 @@ def next(self): class MultiCoupler(LineIterator): + '''多线 coupler,用于在不同长度的 multiline 对象之间保持最近一组可用值。''' + _ltype = LineIterator.IndType def __init__(self): super(MultiCoupler, self).__init__() self.dlen = 0 - self.dsize = self.fullsize() # shorcut for number of lines + self.dsize = self.fullsize() # line 数量的快捷缓存 self.dvals = [float('NaN')] * self.dsize def next(self): @@ -444,28 +453,27 @@ def next(self): def LinesCoupler(cdata, clock=None, **kwargs): if isinstance(cdata, LineSingle): - return SingleCoupler(cdata, clock) # return for single line + return SingleCoupler(cdata, clock) # 单线对象直接返回 SingleCoupler - cdatacls = cdata.__class__ # copy important structures before creation + cdatacls = cdata.__class__ # 创建前复制重要结构 try: - LinesCoupler.counter += 1 # counter for unique class name + LinesCoupler.counter += 1 # 用于唯一类名的计数器 except AttributeError: LinesCoupler.counter = 0 - # Prepare a MultiCoupler subclass + # 准备 MultiCoupler 子类 nclsname = str('LinesCoupler_%d' % LinesCoupler.counter) ncls = type(nclsname, (MultiCoupler,), {}) thismod = sys.modules[LinesCoupler.__module__] setattr(thismod, ncls.__name__, ncls) - # Replace lines et al., to get a sensible clone + # 替换 lines 等结构,得到语义合理的 clone ncls.lines = cdatacls.lines ncls.params = cdatacls.params ncls.plotinfo = cdatacls.plotinfo ncls.plotlines = cdatacls.plotlines - obj = ncls(cdata, **kwargs) # instantiate - # The clock is set here to avoid it being interpreted as a data by the - # LineIterator background scanning code + obj = ncls(cdata, **kwargs) # 实例化 + # 在这里设置 clock,避免它被 LineIterator 的后台扫描逻辑解释为 data if clock is None: clock = getattr(cdata, '_clock', None) if clock is not None: @@ -484,5 +492,5 @@ def LinesCoupler(cdata, clock=None, **kwargs): return obj -# Add an alias (which seems a lot more sensible for "Single Line" lines +# 添加一个别名;对 “Single Line” 来说这个名字更自然 LineCoupler = LinesCoupler diff --git a/backtrader/lineroot.py b/backtrader/lineroot.py index ee5d5aabd..6ce7aed61 100644 --- a/backtrader/lineroot.py +++ b/backtrader/lineroot.py @@ -22,8 +22,8 @@ .. module:: lineroot -Definition of the base class LineRoot and base classes LineSingle/LineMultiple -to define interfaces and hierarchy for the real operational classes +定义 ``LineRoot`` 基类,以及 ``LineSingle`` / ``LineMultiple`` 基类,用于为真正 +执行计算的 line 类建立接口和继承层级。 .. moduleauthor:: Daniel Rodriguez @@ -40,33 +40,29 @@ class MetaLineRoot(metabase.MetaParams): ''' - Once the object is created (effectively pre-init) the "owner" of this - class is sought + ``LineRoot`` 的 metaclass,用于在对象创建后、``__init__`` 前寻找并记录 owner。 ''' def donew(cls, *args, **kwargs): _obj, args, kwargs = super(MetaLineRoot, cls).donew(*args, **kwargs) - # Find the owner and store it - # startlevel = 4 ... to skip intermediate call stacks + # 查找并保存 owner + # startlevel = 4 ... 用于跳过中间调用栈 ownerskip = kwargs.pop('_ownerskip', None) _obj._owner = metabase.findowner(_obj, _obj._OwnerCls or LineMultiple, skip=ownerskip) - # Parameter values have now been set before __init__ + # 参数值此时已经在 __init__ 前设置完毕 return _obj, args, kwargs class LineRoot(with_metaclass(MetaLineRoot, object)): ''' - Defines a common base and interfaces for Single and Multiple - LineXXX instances + ``LineXXX`` 单线和多线实例的基类,用于定义共同接口。 - Period management - Iteration management - Operation (dual/single operand) Management - Rich Comparison operator definition + 主要覆盖 period 管理、迭代管理、单/双操作数运算管理,以及 rich comparison + 操作符定义。 ''' _OwnerCls = None _minperiod = 1 @@ -94,83 +90,148 @@ def _operationown(self, operation): return self._operationown_stage2(operation) def qbuffer(self, savemem=0): - '''Change the lines to implement a minimum size qbuffer scheme''' + '''切换 line,使其使用最小尺寸 qbuffer 方案。 + + Args: + savemem: 是否启用更省内存的 buffer 模式。 + + Returns: + None + ''' raise NotImplementedError def minbuffer(self, size): - '''Receive notification of how large the buffer must at least be''' + '''接收最小 buffer 尺寸通知。 + + Args: + size: buffer 至少需要保证的尺寸。 + + Returns: + None + ''' raise NotImplementedError def setminperiod(self, minperiod): ''' - Direct minperiod manipulation. It could be used for example - by a strategy - to not wait for all indicators to produce a value + 直接设置 minperiod。 + + Args: + minperiod: 要设置的最小 period。 + + Returns: + None + + 例如 strategy 可用它避免等待所有 indicator 都产出值。 ''' self._minperiod = minperiod def updateminperiod(self, minperiod): ''' - Update the minperiod if needed. The minperiod will have been - calculated elsewhere - and has to take over if greater that self's + 在需要时更新 minperiod。 + + Args: + minperiod: 外部计算出的 minperiod;大于当前值时接管当前值。 + + Returns: + None ''' self._minperiod = max(self._minperiod, minperiod) def addminperiod(self, minperiod): ''' - Add a minperiod to own ... to be defined by subclasses + 向自身增加 minperiod,由子类定义具体行为。 + + Args: + minperiod: 要增加的 minperiod。 + + Returns: + None ''' raise NotImplementedError def incminperiod(self, minperiod): ''' - Increment the minperiod with no considerations + 不做额外折算,直接递增 minperiod。 + + Args: + minperiod: 要递增的 minperiod。 + + Returns: + None ''' raise NotImplementedError def prenext(self): ''' - It will be called during the "minperiod" phase of an iteration. + 在迭代的 ``minperiod`` 阶段调用。 + + Returns: + None ''' pass def nextstart(self): ''' - It will be called when the minperiod phase is over for the 1st - post-minperiod value. Only called once and defaults to automatically - calling next + 在 minperiod 阶段结束后的第一个值上调用。 + + Returns: + None + + 该方法只调用一次,默认自动调用 ``next``。 ''' self.next() def next(self): ''' - Called to calculate values when the minperiod is over + minperiod 结束后用于逐 bar 计算值。 + + Returns: + None ''' pass def preonce(self, start, end): ''' - It will be called during the "minperiod" phase of a "once" iteration + 在 ``once`` 迭代的 ``minperiod`` 阶段调用。 + + Args: + start: 起始位置。 + end: 结束位置。 + + Returns: + None ''' pass def oncestart(self, start, end): ''' - It will be called when the minperiod phase is over for the 1st - post-minperiod value + 在 minperiod 阶段结束后的第一个 ``once`` 值上调用。 - Only called once and defaults to automatically calling once + Args: + start: 起始位置。 + end: 结束位置。 + + Returns: + None + + 该方法只调用一次,默认自动调用 ``once``。 ''' self.once(start, end) def once(self, start, end): ''' - Called to calculate values at "once" when the minperiod is over + minperiod 结束后用于批量计算值。 + + Args: + start: 起始位置。 + end: 结束位置。 + + Returns: + None ''' pass - # Arithmetic operators + # 算术操作符 def _makeoperation(self, other, operation, r=False, _ownerskip=None): raise NotImplementedError @@ -179,21 +240,42 @@ def _makeoperationown(self, operation, _ownerskip=None): def _operationown_stage1(self, operation): ''' - Operation with single operand which is "self" + 构建以 ``self`` 为唯一操作数的运算。 + + Args: + operation: 要应用的运算函数。 + + Returns: + LineRoot: 表示该运算的 line 对象。 ''' return self._makeoperationown(operation, _ownerskip=self) def _roperation(self, other, operation, intify=False): ''' - Relies on self._operation to and passes "r" True to define a - reverse operation + 通过 ``self._operation`` 构建反向运算。 + + Args: + other: 另一个操作数。 + operation: 要应用的运算函数。 + intify: 是否把结果转换为整数语义。 + + Returns: + LineRoot: 表示该反向运算的 line 对象或运算结果。 ''' return self._operation(other, operation, r=True, intify=intify) def _operation_stage1(self, other, operation, r=False, intify=False): ''' - Two operands' operation. Scanning of other happens to understand - if other must be directly an operand or rather a subitem thereof + 构建两个操作数的运算。 + + Args: + other: 另一个操作数;如果是 ``LineMultiple``,会使用其第一条 line。 + operation: 要应用的运算函数。 + r: 是否反向运算。 + intify: 是否把结果转换为整数语义。 + + Returns: + LineRoot: 表示该运算的 line 对象。 ''' if isinstance(other, LineMultiple): other = other.lines[0] @@ -202,13 +284,20 @@ def _operation_stage1(self, other, operation, r=False, intify=False): def _operation_stage2(self, other, operation, r=False): ''' - Rich Comparison operators. Scans other and returns either an - operation with other directly or a subitem from other + 在运行阶段执行 rich comparison 或其他即时运算。 + + Args: + other: 另一个操作数;如果是 ``LineRoot``,会取其当前值。 + operation: 要应用的运算函数。 + r: 是否反向运算。 + + Returns: + object: 运算结果。 ''' if isinstance(other, LineRoot): other = other[0] - # operation(float, other) ... expecting other to be a float + # operation(float, other) ... 这里预期 other 是 float if r: return operation(other, self[0]) @@ -288,14 +377,13 @@ def __nonzero__(self): __bool__ = __nonzero__ - # Python 3 forces explicit implementation of hash if - # the class has redefined __eq__ + # Python 3 中,如果类重定义了 __eq__,就必须显式实现 hash __hash__ = object.__hash__ class LineMultiple(LineRoot): ''' - Base class for LineXXX instances that hold more than one line + 多线 ``LineXXX`` 实例的基类,用于管理包含多条 line 的对象。 ''' def reset(self): self._stage1() @@ -313,17 +401,29 @@ def _stage2(self): def addminperiod(self, minperiod): ''' - The passed minperiod is fed to the lines + 把传入的 minperiod 下发给所有 line。 + + Args: + minperiod: 要下发的 minperiod。 + + Returns: + None ''' - # pass it down to the lines + # 下发给所有 line for line in self.lines: line.addminperiod(minperiod) def incminperiod(self, minperiod): ''' - The passed minperiod is fed to the lines + 把传入的 minperiod 递增量下发给所有 line。 + + Args: + minperiod: 要下发的 minperiod 增量。 + + Returns: + None ''' - # pass it down to the lines + # 下发给所有 line for line in self.lines: line.incminperiod(minperiod) @@ -344,16 +444,28 @@ def minbuffer(self, size): class LineSingle(LineRoot): ''' - Base class for LineXXX instances that hold a single line + 单线 ``LineXXX`` 实例的基类,用于管理只包含一条 line 的对象。 ''' def addminperiod(self, minperiod): ''' - Add the minperiod (substracting the overlapping 1 minimum period) + 增加 minperiod,并扣除重叠的 1 个最小 period。 + + Args: + minperiod: 要增加的 minperiod。 + + Returns: + None ''' self._minperiod += minperiod - 1 def incminperiod(self, minperiod): ''' - Increment the minperiod with no considerations + 不做额外折算,直接递增 minperiod。 + + Args: + minperiod: 要递增的 minperiod。 + + Returns: + None ''' self._minperiod += minperiod diff --git a/backtrader/lineseries.py b/backtrader/lineseries.py index babd10572..abbea3eb9 100644 --- a/backtrader/lineseries.py +++ b/backtrader/lineseries.py @@ -22,8 +22,7 @@ .. module:: lineroot -Defines LineSeries and Descriptors inside of it for classes that hold multiple -lines at once. +定义 ``LineSeries`` 及其 descriptor,用于一次持有多条 line 的类。 .. moduleauthor:: Daniel Rodriguez @@ -42,17 +41,16 @@ class LineAlias(object): - ''' Descriptor class that store a line reference and returns that line - from the owner + '''line alias descriptor,用于保存 line 引用并从 owner 返回对应 line。 - Keyword Args: - line (int): reference to the line that will be returned from - owner's *lines* buffer + Args: + line: owner 的 ``lines`` buffer 中要返回的 line 索引。 - As a convenience the __set__ method of the descriptor is used not set - the *line* reference because this is a constant along the live of the - descriptor instance, but rather to set the value of the *line* at the - instant '0' (the current one) + Returns: + LineAlias: 可绑定到类属性上的 line descriptor。 + + 为了使用便利,descriptor 的 ``__set__`` 不会修改 line 引用,因为该引用在 + descriptor 生命周期内是常量;它实际设置的是当前时刻(索引 ``0``)的 line 值。 ''' def __init__(self, line): @@ -63,18 +61,24 @@ def __get__(self, obj, cls=None): def __set__(self, obj, value): ''' - A line cannot be "set" once it has been created. But the values - inside the line can be "set". This is achieved by adding a binding - to the line inside "value" + 设置当前 line 的值。 + + Args: + obj: descriptor 所属对象。 + value: 要绑定或写入的 line 值。 + + Returns: + None + + line 创建后不能替换自身,但可以设置其中的值;这里通过给 ``value`` 内部 line + 添加 binding 来实现。 ''' if isinstance(value, LineMultiple): value = value.lines[0] - # If the now for sure, LineBuffer 'value' is not a LineActions the - # binding below could kick-in too early in the chain writing the value - # into a not yet "forwarded" line, effectively writing the value 1 - # index too early and breaking the functionality (all in next mode) - # Hence the need to transform it into a LineDelay object of null delay + # 如果确定为 LineBuffer 的 value 不是 LineActions,下面的 binding 可能过早触发, + # 把值写入尚未 forward 的 line,等价于提前 1 个索引写入,破坏 next 模式逻辑。 + # 因此需要把它转换为 0 延迟的 LineDelay 对象。 if not isinstance(value, LineActions): value = value(0) @@ -83,13 +87,16 @@ def __set__(self, obj, value): class Lines(object): ''' - Defines an "array" of lines which also has most of the interface of - a LineBuffer class (forward, rewind, advance...). + 多条 line 的容器类,用于提供类似 ``LineBuffer`` 的批量接口。 - This interface operations are passed to the lines held by self + Args: + 无 - The class can autosubclass itself (_derive) to hold new lines keeping them - in the defined order. + Returns: + Lines: 可持有多条 line 的容器。 + + ``forward``、``rewind``、``advance`` 等操作会转发到自身持有的所有 line。 + 该类还可以通过 ``_derive`` 自动派生子类,以按定义顺序保存新 line。 ''' _getlinesbase = classmethod(lambda cls: ()) _getlines = classmethod(lambda cls: ()) @@ -100,15 +107,18 @@ class Lines(object): def _derive(cls, name, lines, extralines, otherbases, linesoverride=False, lalias=None): ''' - Creates a subclass of this class with the lines of this class as - initial input for the subclass. It will include num "extralines" and - lines present in "otherbases" + 创建 ``Lines`` 子类。 - "name" will be used as the suffix of the final class name + Args: + name: 新类名后缀。 + lines: 新增 line 定义。 + extralines: 需要额外创建的 line 数量。 + otherbases: 其他基类中已有的 line 定义。 + linesoverride: 是否丢弃所有基类 line,并以顶层 ``Lines`` 作为基类创建新层级。 + lalias: 额外 line alias 定义。 - "linesoverride": if True the lines of all bases will be discarded and - the baseclass will be the topmost class "Lines". This is intended to - create a new hierarchy + Returns: + type: 派生出的 ``Lines`` 子类。 ''' obaseslines = () obasesextralines = 0 @@ -123,7 +133,7 @@ def _derive(cls, name, lines, extralines, otherbases, linesoverride=False, if not linesoverride: baselines = cls._getlines() + obaseslines baseextralines = cls._getlinesextra() + obasesextralines - else: # overriding lines, skip anything from baseclasses + else: # 覆盖 lines,跳过所有基类内容 baselines = () baseextralines = 0 @@ -131,7 +141,7 @@ def _derive(cls, name, lines, extralines, otherbases, linesoverride=False, clsextralines = baseextralines + extralines lines2add = obaseslines + lines - # str for Python 2/3 compatibility + # str 用于 Python 2/3 兼容 basecls = cls if not linesoverride else Lines newcls = type(str(cls.__name__ + '_' + name), (basecls,), {}) @@ -152,22 +162,20 @@ def _derive(cls, name, lines, extralines, otherbases, linesoverride=False, l2alias = {} if lalias is None else lalias._getkwargsdefault() for line, linealias in l2add: if not isinstance(linealias, string_types): - # a tuple or list was passed, 1st is name + # 传入了 tuple 或 list,第 1 个元素是名称 linealias = linealias[0] - desc = LineAlias(line) # keep a reference below + desc = LineAlias(line) # 保留下面使用的引用 setattr(newcls, linealias, desc) - # Create extra aliases for the given name, checking if the names is in - # l2alias (which is from the argument lalias and comes from the - # directive 'linealias', hence the confusion here (the LineAlias come - # from the directive 'lines') + # 为给定名称创建额外 alias。l2alias 来自参数 lalias,也就是 linealias 指令; + # 这里容易混淆,因为 LineAlias descriptor 来自 lines 指令。 for line, linealias in enumerate(newcls._getlines()): if not isinstance(linealias, string_types): - # a tuple or list was passed, 1st is name + # 传入了 tuple 或 list,第 1 个元素是名称 linealias = linealias[0] - desc = LineAlias(line) # keep a reference below + desc = LineAlias(line) # 保留下面使用的引用 if linealias in l2alias: extranames = l2alias[linealias] if isinstance(linealias, string_types): @@ -181,7 +189,13 @@ def _derive(cls, name, lines, extralines, otherbases, linesoverride=False, @classmethod def _getlinealias(cls, i): ''' - Return the alias for a line given the index + 按索引返回 line alias。 + + Args: + i: line 索引。 + + Returns: + str: line alias,不存在时返回空字符串。 ''' lines = cls._getlines() if i >= len(lines): @@ -198,15 +212,20 @@ def itersize(self): def __init__(self, initlines=None): ''' - Create the lines recording during "_derive" or else use the - provided "initlines" + 创建 ``_derive`` 记录的 line,或使用传入的 ``initlines``。 + + Args: + initlines: 可选的初始 line 列表。 + + Returns: + None ''' self.lines = list() for line, linealias in enumerate(self._getlines()): kwargs = dict() self.lines.append(LineBuffer(**kwargs)) - # Add the required extralines + # 添加所需的 extralines for i in range(self._getlinesextra()): if not initlines: self.lines.append(LineBuffer()) @@ -215,7 +234,7 @@ def __init__(self, initlines=None): def __len__(self): ''' - Proxy line operation + 代理到第 1 条 line 的长度操作。 ''' return len(self.lines[0]) @@ -230,152 +249,142 @@ def extrasize(self): def __getitem__(self, line): ''' - Proxy line operation + 按索引获取 line。 ''' return self.lines[line] def get(self, ago=0, size=1, line=0): ''' - Proxy line operation + 代理到指定 line 的 ``get`` 操作。 ''' return self.lines[line].get(ago, size=size) def __setitem__(self, line, value): ''' - Proxy line operation + 代理到指定 line 的设置操作。 ''' setattr(self, self._getlinealias(line), value) def forward(self, value=NAN, size=1): ''' - Proxy line operation + 对所有 line 执行 ``forward``。 ''' for line in self.lines: line.forward(value, size=size) def backwards(self, size=1, force=False): ''' - Proxy line operation + 对所有 line 执行 ``backwards``。 ''' for line in self.lines: line.backwards(size, force=force) def rewind(self, size=1): ''' - Proxy line operation + 对所有 line 执行 ``rewind``。 ''' for line in self.lines: line.rewind(size) def extend(self, value=NAN, size=0): ''' - Proxy line operation + 对所有 line 执行 ``extend``。 ''' for line in self.lines: line.extend(value, size) def reset(self): ''' - Proxy line operation + 对所有 line 执行 ``reset``。 ''' for line in self.lines: line.reset() def home(self): ''' - Proxy line operation + 对所有 line 执行 ``home``。 ''' for line in self.lines: line.home() def advance(self, size=1): ''' - Proxy line operation + 对所有 line 执行 ``advance``。 ''' for line in self.lines: line.advance(size) def buflen(self, line=0): ''' - Proxy line operation + 返回指定 line 的 buffer 长度。 ''' return self.lines[line].buflen() class MetaLineSeries(LineMultiple.__class__): ''' - Dirty job manager for a LineSeries - - - During __new__ (class creation), it reads "lines", "plotinfo", - "plotlines" class variable definitions and turns them into - Classes of type Lines or AutoClassInfo (plotinfo/plotlines) + ``LineSeries`` 的 metaclass,用于管理 line、plotinfo 和 plotlines 的类创建与实例创建。 - - During "new" (instance creation) the lines/plotinfo/plotlines - classes are substituted in the instance with instances of the - aforementioned classes and aliases are added for the "lines" held - in the "lines" instance + 在 ``__new__`` 阶段读取 ``lines``、``plotinfo``、``plotlines`` 类变量定义,并将 + 它们转换为 ``Lines`` 或 ``AutoClassInfo`` 类型的类。 - Additionally and for remaining kwargs, these are matched against - args in plotinfo and if existent are set there and removed from kwargs + 在实例创建阶段,将这些类替换为对应实例,并为 ``lines`` 实例中持有的 line 添加 + alias。 - Remember that this Metaclass has a MetaParams (from metabase) - as root class and therefore "params" defined for the class have been - removed from kwargs at an earlier state + 剩余 ``kwargs`` 会与 ``plotinfo`` 参数匹配;匹配到的会设置到 ``plotinfo`` 并从 + ``kwargs`` 中移除。该 metaclass 的根类是 ``MetaParams``,因此类中定义的 + ``params`` 已在更早阶段从 ``kwargs`` 中移除。 ''' def __new__(meta, name, bases, dct): ''' - Intercept class creation, identifiy lines/plotinfo/plotlines class - attributes and create corresponding classes for them which take over - the class attributes + 拦截类创建,识别 ``lines`` / ``plotinfo`` / ``plotlines`` 类属性,并创建对应类 + 接管这些属性。 ''' - # Get the aliases - don't leave it there for subclasses + # 获取 alias,不把它留给子类继续处理 aliases = dct.setdefault('alias', ()) aliased = dct.setdefault('aliased', '') - # Remove the line definition (if any) from the class creation + # 从类创建字典中取出 line 定义(如果存在) linesoverride = dct.pop('linesoverride', False) newlines = dct.pop('lines', ()) extralines = dct.pop('extralines', 0) - # remove the new plotinfo/plotlines definition if any + # 取出新的 linealias 定义(如果存在) newlalias = dict(dct.pop('linealias', {})) - # remove the new plotinfo/plotlines definition if any + # 取出新的 plotinfo/plotlines 定义(如果存在) newplotinfo = dict(dct.pop('plotinfo', {})) newplotlines = dict(dct.pop('plotlines', {})) - # Create the class - pulling in any existing "lines" + # 创建类,同时带入已有 lines cls = super(MetaLineSeries, meta).__new__(meta, name, bases, dct) - # Check the line aliases before creating the lines + # 创建 lines 前先检查 line alias lalias = getattr(cls, 'linealias', AutoInfoClass) oblalias = [x.linealias for x in bases[1:] if hasattr(x, 'linealias')] cls.linealias = la = lalias._derive('la_' + name, newlalias, oblalias) - # Get the actual lines or a default + # 获取实际 lines,或使用默认值 lines = getattr(cls, 'lines', Lines) - # Create a subclass of the lines class with our name and newlines - # and put it in the class + # 创建带当前类名和新增 line 的 lines 子类,并放回类中 morebaseslines = [x.lines for x in bases[1:] if hasattr(x, 'lines')] cls.lines = lines._derive(name, newlines, extralines, morebaseslines, linesoverride, lalias=la) - # Get a copy from base class plotinfo/plotlines (created with the - # class or set a default) + # 从基类获取 plotinfo/plotlines 副本;不存在时使用默认值 plotinfo = getattr(cls, 'plotinfo', AutoInfoClass) plotlines = getattr(cls, 'plotlines', AutoInfoClass) - # Create a plotinfo/plotlines subclass and set it in the class + # 创建 plotinfo/plotlines 子类并放回类中 morebasesplotinfo = \ [x.plotinfo for x in bases[1:] if hasattr(x, 'plotinfo')] cls.plotinfo = plotinfo._derive('pi_' + name, newplotinfo, morebasesplotinfo) - # Before doing plotline newlines have been added and no plotlineinfo - # is there add a default + # 处理 plotline 前,新增 line 已加入;若没有对应 plotlineinfo,则补默认项 for line in newlines: newplotlines.setdefault(line, dict()) @@ -384,14 +393,14 @@ def __new__(meta, name, bases, dct): cls.plotlines = plotlines._derive( 'pl_' + name, newplotlines, morebasesplotlines, recurse=True) - # create declared class aliases (a subclass with no modifications) + # 创建声明过的类 alias(无修改子类) for alias in aliases: newdct = {'__doc__': cls.__doc__, '__module__': cls.__module__, 'aliased': cls.__name__} if not isinstance(alias, string_types): - # a tuple or list was passed, 1st is name, 2nd plotname + # 传入了 tuple 或 list,第 1 个元素是名称,第 2 个元素是 plotname aliasplotname = alias[1] alias = alias[0] newdct['plotinfo'] = dict(plotname=aliasplotname) @@ -400,34 +409,33 @@ def __new__(meta, name, bases, dct): clsmodule = sys.modules[cls.__module__] setattr(clsmodule, alias, newcls) - # return the class + # 返回创建出的类 return cls def donew(cls, *args, **kwargs): ''' - Intercept instance creation, take over lines/plotinfo/plotlines - class attributes by creating corresponding instance variables and add - aliases for "lines" and the "lines" held within it + 拦截实例创建,创建对应实例变量来接管 ``lines`` / ``plotinfo`` / ``plotlines`` + 类属性,并为 ``lines`` 及其内部 line 添加 alias。 ''' - # _obj.plotinfo shadows the plotinfo (class) definition in the class + # _obj.plotinfo 会遮蔽类中的 plotinfo 定义 plotinfo = cls.plotinfo() for pname, pdef in cls.plotinfo._getitems(): setattr(plotinfo, pname, kwargs.pop(pname, pdef)) - # Create the object and set the params in place + # 创建对象并设置参数 _obj, args, kwargs = super(MetaLineSeries, cls).donew(*args, **kwargs) - # set the plotinfo member in the class + # 设置对象上的 plotinfo 成员 _obj.plotinfo = plotinfo - # _obj.lines shadows the lines (class) definition in the class + # _obj.lines 会遮蔽类中的 lines 定义 _obj.lines = cls.lines() - # _obj.plotinfo shadows the plotinfo (class) definition in the class + # _obj.plotlines 会遮蔽类中的 plotlines 定义 _obj.plotlines = cls.plotlines() - # add aliases for lines and for the lines class itself + # 为 lines 和 lines 类本身添加 alias _obj.l = _obj.lines if _obj.lines.fullsize(): _obj.line = _obj.lines[0] @@ -437,11 +445,13 @@ class attributes by creating corresponding instance variables and add setattr(_obj, 'line_%d' % l, line) setattr(_obj, 'line%d' % l, line) - # Parameter values have now been set before __init__ + # 参数值此时已经在 __init__ 前设置完毕 return _obj, args, kwargs class LineSeries(with_metaclass(MetaLineSeries, LineMultiple)): + '''多线序列的基类,用于承载 lines、plotinfo 和 plotlines。''' + plotinfo = dict( plot=True, plotmaster=None, @@ -455,9 +465,8 @@ def array(self): return self.lines[0].array def __getattr__(self, name): - # to refer to line by name directly if the attribute was not found - # in this object if we set an attribute in this object it will be - # found before we end up here + # 对象自身找不到属性时,允许按 line 名称直接引用 line。 + # 如果对象自身设置了同名属性,会在到达这里前被找到。 return getattr(self.lines, name) def __len__(self): @@ -470,10 +479,9 @@ def __setitem__(self, key, value): setattr(self.lines, self.lines._getlinealias(key), value) def __init__(self, *args, **kwargs): - # if any args, kwargs make it up to here, something is broken - # defining a __init__ guarantees the existence of im_func to findbases - # in lineiterator later, because object.__init__ has no im_func - # (object has slots) + # 如果有 args/kwargs 传到这里,说明上游处理有问题。 + # 定义 __init__ 可保证后续 lineiterator 的 findbases 能找到 im_func; + # object.__init__ 没有 im_func(object 使用 slots)。 super(LineSeries, self).__init__() pass @@ -482,7 +490,7 @@ def plotlabel(self): sublabels = self._plotlabel() if sublabels: for i, sublabel in enumerate(sublabels): - # if isinstance(sublabel, LineSeries): ## DOESN'T WORK ??? + # if isinstance(sublabel, LineSeries): ## 不可用 ??? if hasattr(sublabel, 'plotinfo'): try: s = sublabel.plotinfo.plotname @@ -501,8 +509,8 @@ def _getline(self, line, minusall=False): if isinstance(line, string_types): lineobj = getattr(self.lines, line) else: - if line == -1: # restore original api behavior - default -> 0 - if minusall: # minus means ... all lines + if line == -1: # 恢复原 API 行为:默认值 -> 0 + if minusall: # 负数表示所有 line return None line = 0 lineobj = self.lines[line] @@ -510,31 +518,19 @@ def _getline(self, line, minusall=False): return lineobj def __call__(self, ago=None, line=-1): - '''Returns either a delayed verison of itself in the form of a - LineDelay object or a timeframe adapting version with regards to a ago - - Param: ago (default: None) + '''返回自身的延迟版本,或按 timeframe 适配后的版本。 - If ago is None or an instance of LineRoot (a lines object) the - returned valued is a LineCoupler instance + Args: + ago: ``None`` 或 ``LineRoot`` 时返回 ``LinesCoupler``;其他情况按整数处理, + 返回 ``LineDelay``。 + line: 要引用的 line 名称或索引。返回 ``LinesCoupler`` 时,``-1`` 表示适配 + 当前 ``LineMultiple`` 对象的全部 line;否则只适配指定 line。返回 + ``LineDelay`` 时,``-1`` 等同于 ``0``,用于保留旧默认行为。 - If ago is anything else, it is assumed to be an int and a LineDelay - object will be returned - - Param: line (default: -1) - If a LinesCoupler will be returned ``-1`` means to return a - LinesCoupler which adapts all lines of the current LineMultiple - object. Else the appropriate line (referenced by name or index) will - be LineCoupled - - If a LineDelay object will be returned, ``-1`` is the same as ``0`` - (to retain compatibility with the previous default value of 0). This - behavior will change to return all existing lines in a LineDelayed - form - - The referenced line (index or name) will be LineDelayed + Returns: + LinesCoupler | LineDelay: 适配或延迟后的 line 对象。 ''' - from .lineiterator import LinesCoupler # avoid circular import + from .lineiterator import LinesCoupler # 避免循环 import if ago is None or isinstance(ago, LineRoot): args = [self, ago] @@ -544,12 +540,11 @@ def __call__(self, ago=None, line=-1): return LinesCoupler(*args, _ownerskip=self) - # else -> assume type(ago) == int -> return LineDelay object + # 其他情况假定 ago 是 int,返回 LineDelay 对象 return LineDelay(self._getline(line), ago, _ownerskip=self) - # The operations below have to be overriden to make sure subclasses can - # reach them using "super" which will not call __getattr__ and - # LineSeriesStub (see below) already uses super + # 下列操作必须覆写,确保子类可以通过 super 访问;super 不会调用 __getattr__, + # 而下面的 LineSeriesStub 已经使用 super。 def forward(self, value=NAN, size=1): self.lines.forward(value, size) @@ -573,33 +568,30 @@ def advance(self, size=1): class LineSeriesStub(LineSeries): - '''Simulates a LineMultiple object based on LineSeries from a single line - - The index management operations are overriden to take into account if the - line is a slave, ie: + '''基于单条 line 模拟 ``LineMultiple`` 对象的 stub。 - - The line reference is a line from many in a LineMultiple object - - Both the LineMultiple object and the Line are managed by the same - object + Args: + line: 要包装的单条 line。 + slave: 是否作为从属 line 使用。 - Were slave not to be taken into account, the individual line would for - example be advanced twice: + Returns: + LineSeriesStub: 可当作 ``LineSeries`` 使用的单 line 包装对象。 - - Once under when the LineMultiple object is advanced (because it - advances all lines it is holding - - Again as part of the regular management of the object holding it + index 管理操作会考虑该 line 是否为 slave。若不考虑 slave,单独 line 可能被推进 + 两次:一次来自 ``LineMultiple`` 推进所有持有 line,另一次来自持有它的对象的常规 + 管理。 ''' extralines = 1 def __init__(self, line, slave=False): self.lines = self.__class__.lines(initlines=[line]) - # give a change to find the line owner (for plotting at least) + # 给外部机会找到 line owner(至少绘图需要) self.owner = self._owner = line._owner self._minperiod = line._minperiod self.slave = slave - # Only execute the operations below if the object is not a slave + # 只有对象不是 slave 时才执行下列操作 def forward(self, value=NAN, size=1): if not self.slave: super(LineSeriesStub, self).forward(value, size) diff --git a/backtrader/mathsupport.py b/backtrader/mathsupport.py index 0ffec0653..c6c51a045 100644 --- a/backtrader/mathsupport.py +++ b/backtrader/mathsupport.py @@ -25,26 +25,37 @@ def average(x, bessel=False): - ''' - Args: - x: iterable with len + '''计算序列的平均值。 - oneless: (default ``False``) reduces the length of the array for the - division. + Args: + x: 支持 ``len`` 的 iterable。 + bessel: 是否使用 ``N - 1`` 作为分母,常用于 Bessel 校正。 Returns: - A float with the average of the elements of x + float: ``x`` 中元素的平均值。 + + --- + 交互示例: + >>> average([1, 2, 3]) + 2.0 ''' return math.fsum(x) / (len(x) - bessel) def variance(x, avgx=None): - ''' + '''计算序列每个元素相对平均值的平方偏差。 + Args: - x: iterable with len + x: 支持 ``len`` 的 iterable。 + avgx: 已预先计算好的平均值;默认 ``None`` 时内部调用 ``average``。 Returns: - A list with the variance for each element of x + list: ``x`` 中每个元素的平方偏差。 + + --- + 交互示例: + >>> variance([1, 2, 3]) + [1.0, 0.0, 1.0] ''' if avgx is None: avgx = average(x) @@ -52,14 +63,19 @@ def variance(x, avgx=None): def standarddev(x, avgx=None, bessel=False): - ''' - Args: - x: iterable with len + '''计算序列的标准差。 - bessel: (default ``False``) to be passed to the average to divide by - ``N - 1`` (Bessel's correction) + Args: + x: 支持 ``len`` 的 iterable。 + avgx: 已预先计算好的平均值;默认 ``None`` 时内部计算。 + bessel: 是否按 ``N - 1`` 作为分母应用 Bessel 校正。 Returns: - A float with the standard deviation of the elements of x + float: ``x`` 中元素的标准差。 + + --- + 交互示例: + >>> round(standarddev([1, 2, 3]), 6) + 0.816497 ''' return math.sqrt(average(variance(x, avgx), bessel=bessel)) diff --git a/backtrader/metabase.py b/backtrader/metabase.py index 8f19cbb89..81ab911c1 100644 --- a/backtrader/metabase.py +++ b/backtrader/metabase.py @@ -40,21 +40,21 @@ def findbases(kls, topclass): def findowner(owned, cls, startlevel=2, skip=None): - # skip this frame and the caller's -> start at 2 + # 跳过当前 frame 和调用者 frame,因此从 2 开始 for framelevel in itertools.count(startlevel): try: frame = sys._getframe(framelevel) except ValueError: - # Frame depth exceeded ... no owner ... break away + # frame 深度超限,找不到 owner,退出 break - # 'self' in regular code + # 常规代码中的 'self' self_ = frame.f_locals.get('self', None) if skip is not self_: if self_ is not owned and isinstance(self_, cls): return self_ - # '_obj' in metaclasses + # metaclasses 中的 '_obj' obj_ = frame.f_locals.get('_obj', None) if skip is not obj_: if obj_ is not owned and isinstance(obj_, cls): @@ -97,7 +97,7 @@ class AutoInfoClass(object): @classmethod def _derive(cls, name, info, otherbases, recurse=False): - # collect the 3 set of infos + # 收集 3 组 infos # info = OrderedDict(info) baseinfo = cls._getpairs().copy() obasesinfo = OrderedDict() @@ -107,26 +107,24 @@ def _derive(cls, name, info, otherbases, recurse=False): else: obasesinfo.update(obase._getpairs()) - # update the info of this class (base) with that from the other bases + # 用其他 bases 的信息更新当前 class/base 的信息 baseinfo.update(obasesinfo) - # The info of the new class is a copy of the full base info - # plus and update from parameter + # 新 class 的 info 是完整 base info 的副本,再叠加参数中的更新 clsinfo = baseinfo.copy() clsinfo.update(info) - # The new items to update/set are those from the otherbase plus the new + # 需要 update/set 的新项来自 otherbase 与新增 info info2add = obasesinfo.copy() info2add.update(info) clsmodule = sys.modules[cls.__module__] newclsname = str(cls.__name__ + '_' + name) # str - Python 2/3 compat - # This loop makes sure that if the name has already been defined, a new - # unique name is found. A collision example is in the plotlines names - # definitions of bt.indicators.MACD and bt.talib.MACD. Both end up - # definining a MACD_pl_macd and this makes it impossible for the pickle - # module to send results over a multiprocessing channel + # 该循环确保当 name 已经定义时,可以找到新的唯一名称。 + # 一个冲突示例是 bt.indicators.MACD 和 bt.talib.MACD 的 plotlines 名称定义: + # 两者最终都会定义 MACD_pl_macd,导致 pickle 无法通过 multiprocessing channel + # 发送结果 namecounter = 1 while hasattr(clsmodule, newclsname): newclsname += str(namecounter) @@ -202,30 +200,29 @@ def __new__(cls, *args, **kwargs): class MetaParams(MetaBase): def __new__(meta, name, bases, dct): - # Remove params from class definition to avoid inheritance - # (and hence "repetition") + # 从 class 定义中移除 params,以避免继承导致的重复 newparams = dct.pop('params', ()) packs = 'packages' - newpackages = tuple(dct.pop(packs, ())) # remove before creation + newpackages = tuple(dct.pop(packs, ())) # 创建前移除 fpacks = 'frompackages' - fnewpackages = tuple(dct.pop(fpacks, ())) # remove before creation + fnewpackages = tuple(dct.pop(fpacks, ())) # 创建前移除 - # Create the new class - this pulls predefined "params" + # 创建新 class,这会拉取预定义的 "params" cls = super(MetaParams, meta).__new__(meta, name, bases, dct) - # Pulls the param class out of it - default is the empty class + # 取出 param class,默认为空 class params = getattr(cls, 'params', AutoInfoClass) - # Pulls the packages class out of it - default is the empty class + # 取出 packages class,默认为空 class packages = tuple(getattr(cls, packs, ())) fpackages = tuple(getattr(cls, fpacks, ())) - # get extra (to the right) base classes which have a param attribute + # 获取右侧额外 bases 中带 param 属性的 class morebasesparams = [x.params for x in bases[1:] if hasattr(x, 'params')] - # Get extra packages, add them to the packages and put all in the class + # 获取额外 packages,合并后写回 class for y in [x.packages for x in bases[1:] if hasattr(x, packs)]: packages += tuple(y) @@ -235,14 +232,14 @@ def __new__(meta, name, bases, dct): cls.packages = packages + newpackages cls.frompackages = fpackages + fnewpackages - # Subclass and store the newly derived params class + # 派生并保存新的 params class cls.params = params._derive(name, newparams, morebasesparams) return cls def donew(cls, *args, **kwargs): clsmod = sys.modules[cls.__module__] - # import specified packages + # import 指定 packages for p in cls.packages: if isinstance(p, (tuple, list)): p, palias = p @@ -252,57 +249,55 @@ def donew(cls, *args, **kwargs): pmod = __import__(p) plevels = p.split('.') - if p == palias and len(plevels) > 1: # 'os.path' not aliased - setattr(clsmod, pmod.__name__, pmod) # set 'os' in module + if p == palias and len(plevels) > 1: # 'os.path' 未使用 alias + setattr(clsmod, pmod.__name__, pmod) # 在 module 中设置 'os' - else: # aliased and/or dots - for plevel in plevels[1:]: # recurse down the mod + else: # 使用 alias 和/或 dotted path + for plevel in plevels[1:]: # 沿 module 层级向下递归 pmod = getattr(pmod, plevel) setattr(clsmod, palias, pmod) - # import from specified packages - the 2nd part is a string or iterable + # 从指定 packages import,第 2 部分可以是字符串或 iterable for p, frompackage in cls.frompackages: if isinstance(frompackage, string_types): - frompackage = (frompackage,) # make it a tuple + frompackage = (frompackage,) # 转成 tuple for fp in frompackage: if isinstance(fp, (tuple, list)): fp, falias = fp else: - fp, falias = fp, fp # assumed is string + fp, falias = fp, fp # 假设为字符串 - # complain "not string" without fp (unicode vs bytes) + # 使用 str(fp) 规避 unicode/bytes 场景下的非字符串报错 pmod = __import__(p, fromlist=[str(fp)]) pattr = getattr(pmod, fp) setattr(clsmod, falias, pattr) for basecls in cls.__bases__: setattr(sys.modules[basecls.__module__], falias, pattr) - # Create params and set the values from the kwargs + # 创建 params,并从 kwargs 设置值 params = cls.params() for pname, pdef in cls.params._getitems(): setattr(params, pname, kwargs.pop(pname, pdef)) - # Create the object and set the params in place + # 创建对象并设置 params _obj, args, kwargs = super(MetaParams, cls).donew(*args, **kwargs) _obj.params = params _obj.p = params # shorter alias - # Parameter values have now been set before __init__ + # parameter values 已在 __init__ 前设置完毕 return _obj, args, kwargs class ParamsBase(with_metaclass(MetaParams, object)): - pass # stub to allow easy subclassing without metaclasses + pass # stub,用于在不直接处理 metaclasses 的情况下方便 subclassing class ItemCollection(object): - ''' - Holds a collection of items that can be reached by + '''保存一组可通过索引或名称访问的 items。 - - Index - - Name (if set in the append operation) + append 时如果设置了名称,之后即可通过名称访问。 ''' def __init__(self): self._items = list() diff --git a/backtrader/observer.py b/backtrader/observer.py index 5b41be65b..4f4f5c66e 100644 --- a/backtrader/observer.py +++ b/backtrader/observer.py @@ -27,23 +27,27 @@ class MetaObserver(ObserverBase.__class__): + '''Observer metaclass 的基类,用于完成 observer 初始化挂接。''' + def donew(cls, *args, **kwargs): _obj, args, kwargs = super(MetaObserver, cls).donew(*args, **kwargs) - _obj._analyzers = list() # keep children analyzers + _obj._analyzers = list() # 保存子 analyzer - return _obj, args, kwargs # return the instantiated object and args + return _obj, args, kwargs # 返回实例化对象和参数 def dopreinit(cls, _obj, *args, **kwargs): _obj, args, kwargs = \ super(MetaObserver, cls).dopreinit(_obj, *args, **kwargs) - if _obj._stclock: # Change clock if strategy wide observer + if _obj._stclock: # strategy-wide observer 使用 strategy clock _obj._clock = _obj._owner return _obj, args, kwargs class Observer(with_metaclass(MetaObserver, ObserverBase)): + '''Observer 的基类,用于在 strategy 运行时观察并记录状态。''' + _stclock = False _OwnerCls = StrategyBase @@ -53,8 +57,8 @@ class Observer(with_metaclass(MetaObserver, ObserverBase)): plotinfo = dict(plot=False, subplot=True) - # An Observer is ideally always observing and that' why prenext calls - # next. The behaviour can be overriden by subclasses + # Observer 理想情况下应始终观察,因此 prenext 调用 next。 + # 子类可以覆盖该行为。 def prenext(self): self.next() diff --git a/backtrader/observers/__init__.py b/backtrader/observers/__init__.py index 48690e57f..10e10a538 100644 --- a/backtrader/observers/__init__.py +++ b/backtrader/observers/__init__.py @@ -21,8 +21,8 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) -# The modules below should/must define __all__ with the Indicator objects -# of prepend an "_" (underscore) to private classes/variables +# 下方模块应定义 __all__ 来声明 Indicator 对象,或给私有 class/variable 加上 +# "_" 前缀 from .broker import * from .buysell import * diff --git a/backtrader/observers/benchmark.py b/backtrader/observers/benchmark.py index 042f8c027..b13ee9902 100644 --- a/backtrader/observers/benchmark.py +++ b/backtrader/observers/benchmark.py @@ -26,56 +26,32 @@ class Benchmark(TimeReturn): - '''This observer stores the *returns* of the strategy and the *return* of a - reference asset which is one of the datas passed to the system. - - Params: - - - ``timeframe`` (default: ``None``) - If ``None`` then the complete return over the entire backtested period - will be reported - - - ``compression`` (default: ``None``) - - Only used for sub-day timeframes to for example work on an hourly - timeframe by specifying "TimeFrame.Minutes" and 60 as compression - - - ``data`` (default: ``None``) - - Reference asset to track to allow for comparison. - - .. note:: this data must have been added to a ``cerebro`` instance with - ``addata``, ``resampledata`` or ``replaydata``. - - - - ``_doprenext`` (default: ``False``) - - Benchmarking will take place from the point at which the strategy kicks - in (i.e.: when the minimum period of the strategy has been met). - - Setting this to ``True`` will record benchmarking values from the - starting point of the data feeds - - - ``firstopen`` (default: ``False``) - - Keepint it as ``False`` ensures that the 1st comparison point between - the value and the benchmark starts at 0%, because the benchmark will - not use its opening price. - - See the ``TimeReturn`` analyzer reference for a full explanation of the - meaning of the parameter - - - ``fund`` (default: ``None``) - - If ``None`` the actual mode of the broker (fundmode - True/False) will - be autodetected to decide if the returns are based on the total net - asset value or on the fund value. See ``set_fundmode`` in the broker - documentation - - Set it to ``True`` or ``False`` for a specific behavior - - Remember that at any moment of a ``run`` the current values can be checked - by looking at the *lines* by name at index ``0``. + '''存储 strategy return 和参考资产 return 的 observer。 + + 参考资产是传入系统的某个 data,用于和 strategy 表现进行对比。 + + Args: + timeframe: 统计使用的 timeframe,默认 ``None``。如果为 ``None``, + 报告整个 backtest period 的完整 return。 + compression: timeframe 压缩倍数,默认 ``None``。仅用于日内 + timeframe。 + data: 用于对比的参考资产,默认 ``None``。该 data 必须已经通过 + ``adddata``、``resampledata`` 或 ``replaydata`` 加入 ``cerebro``。 + _doprenext (bool): 是否从 data feed 起点就记录 benchmark,默认 + ``False``。默认情况下会从 strategy 最小 period 满足后开始记录。 + firstopen (bool): 默认 ``False``,确保第 1 个 value 与 benchmark 的 + 比较点从 0% 开始。完整含义参见 ``TimeReturn`` analyzer。 + fund: 如果为 ``None``,会自动检测 broker 的实际模式(fundmode - + True/False),以决定 returns 基于总净资产 value 还是 fund value。 + 将其设为 ``True`` 或 ``False`` 可指定具体行为。 + + Returns: + None: observer 通过 ``benchmark`` line 暴露当前值。 + + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addobserver(Benchmark) ''' _stclock = True @@ -86,7 +62,7 @@ class Benchmark(TimeReturn): params = ( ('data', None), ('_doprenext', False), - # Set to false to ensure the asset is measured at 0% in the 1st tick + # 设为 False,确保资产在第 1 个 tick 以 0% 开始衡量 ('firstopen', False), ('fund', None) ) @@ -97,16 +73,16 @@ def _plotlabel(self): return labels def __init__(self): - if self.p.data is None: # use the 1st data in the system if none given + if self.p.data is None: # 未指定时使用系统中的第 1 个 data self.p.data = self.data0 - super(Benchmark, self).__init__() # treturn including data parameter - # Create a time return object without the data + super(Benchmark, self).__init__() # 包含 data 参数的 treturn + # 创建不带 data 的 time return 对象 kwargs = self.p._getkwargs() - kwargs.update(data=None) # to create a return for the stratey + kwargs.update(data=None) # 用于创建 strategy return t = self._owner._addanalyzer_slave(bt.analyzers.TimeReturn, **kwargs) - # swap for consistency + # 交换以保持一致性 self.treturn, self.tbench = t, self.treturn def next(self): diff --git a/backtrader/observers/broker.py b/backtrader/observers/broker.py index 85b42733b..21ad0e799 100644 --- a/backtrader/observers/broker.py +++ b/backtrader/observers/broker.py @@ -25,9 +25,17 @@ class Cash(Observer): - '''This observer keeps track of the current amount of cash in the broker + '''该 observer 跟踪 broker 中当前的 cash 数量 - Params: None + Args: + 无。 + + --- + 交互示例: + + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addobserver(Cash) ''' _stclock = True @@ -40,20 +48,20 @@ def next(self): class Value(Observer): - '''This observer keeps track of the current portfolio value in the broker - including the cash - - Params: + '''该 observer 跟踪 broker 中当前的 portfolio value,包括 cash - - ``fund`` (default: ``None``) + Args: + fund: 如果为 ``None``,会自动检测 broker 的实际模式(fundmode - + True/False),以决定 value 是基于总净资产 value 还是 fund value。 + 参见 broker 文档中的 ``set_fundmode``。将其设为 ``True`` 或 + ``False`` 可指定具体行为。 - If ``None`` the actual mode of the broker (fundmode - True/False) will - be autodetected to decide if the returns are based on the total net - asset value or on the fund value. See ``set_fundmode`` in the broker - documentation - - Set it to ``True`` or ``False`` for a specific behavior + --- + 交互示例: + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addobserver(Value, fund=False) ''' _stclock = True @@ -79,10 +87,20 @@ def next(self): class Broker(Observer): - '''This observer keeps track of the current cash amount and portfolio value in - the broker (including the cash) + '''该 observer 跟踪 broker 中当前的 cash 数量和 portfolio value + (包括 cash) + + Args: + fund: 如果为 ``None``,会自动检测 broker 的实际 fundmode。设为 + ``True`` 时按 fund value 展示,设为 ``False`` 时按普通 broker + value/cash 展示。 - Params: None + --- + 交互示例: + + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addobserver(Broker) ''' _stclock = True @@ -114,9 +132,17 @@ def next(self): class FundValue(Observer): - '''This observer keeps track of the current fund-like value + '''该 observer 跟踪当前类 fund value + + Args: + 无。 + + --- + 交互示例: - Params: None + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addobserver(FundValue) ''' _stclock = True @@ -130,9 +156,17 @@ def next(self): class FundShares(Observer): - '''This observer keeps track of the current fund-like shares + '''该 observer 跟踪当前类 fund shares + + Args: + 无。 + + --- + 交互示例: - Params: None + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addobserver(FundShares) ''' _stclock = True diff --git a/backtrader/observers/buysell.py b/backtrader/observers/buysell.py index 2fe067b59..4b586132e 100644 --- a/backtrader/observers/buysell.py +++ b/backtrader/observers/buysell.py @@ -27,20 +27,22 @@ class BuySell(Observer): - ''' - This observer keeps track of the individual buy/sell orders (individual - executions) and will plot them on the chart along the data around the - execution price level - - Params: - - ``barplot`` (default: ``False``) Plot buy signals below the minimum and - sell signals above the maximum. - - If ``False`` it will plot on the average price of executions during a - bar - - - ``bardist`` (default: ``0.015`` 1.5%) Distance to max/min when - ``barplot`` is ``True`` + '''跟踪单笔 buy/sell execution,并在图上标出执行价附近位置的 observer。 + + Args: + barplot (bool): 是否把 buy 信号画在 bar 最低价下方、sell 信号画在最高价 + 上方,默认 ``False``。如果为 ``False``,会画在该 bar 内 execution + 的平均价上。 + bardist (float): ``barplot`` 为 ``True`` 时,距离最高/最低价的比例, + 默认 ``0.015``(1.5%)。 + + Returns: + None: observer 通过 ``buy`` 和 ``sell`` lines 暴露当前值。 + + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addobserver(BuySell, barplot=True) ''' lines = ('buy', 'sell',) @@ -53,8 +55,8 @@ class BuySell(Observer): ) params = ( - ('barplot', False), # plot above/below max/min for clarity in bar plot - ('bardist', 0.015), # distance to max/min in absolute perc + ('barplot', False), # bar plot 中画在最高/最低价之外以增强可读性 + ('bardist', 0.015), # 到最高/最低价的绝对比例距离 ) def next(self): @@ -70,8 +72,8 @@ def next(self): else: sell.append(order.executed.price) - # Take into account replay ... something could already be in there - # Write down the average buy/sell price + # 考虑 replay 场景:line 中可能已有值 + # 写入平均 buy/sell price # BUY curbuy = self.lines.buy[0] @@ -87,11 +89,11 @@ def next(self): value = buyops / float(buylen or 'NaN') if not self.p.barplot: self.lines.buy[0] = value - elif value == value: # Not NaN + elif value == value: # 不是 NaN pbuy = self.data.low[0] * (1 - self.p.bardist) self.lines.buy[0] = pbuy - # Update buylen values + # 更新 buylen 值 curbuy = buyops self.curbuylen = buylen @@ -109,10 +111,10 @@ def next(self): value = sellops / float(selllen or 'NaN') if not self.p.barplot: self.lines.sell[0] = value - elif value == value: # Not NaN + elif value == value: # 不是 NaN psell = self.data.high[0] * (1 + self.p.bardist) self.lines.sell[0] = psell - # Update selllen values + # 更新 selllen 值 cursell = sellops self.curselllen = selllen diff --git a/backtrader/observers/drawdown.py b/backtrader/observers/drawdown.py index e91ba295f..a12fd0087 100644 --- a/backtrader/observers/drawdown.py +++ b/backtrader/observers/drawdown.py @@ -26,19 +26,22 @@ class DrawDown(Observer): - '''This observer keeps track of the current drawdown level (plotted) and - the maxdrawdown (not plotted) levels + '''跟踪当前 drawdown 和 maxdrawdown 的 observer。 - Params: + 当前 drawdown 会被绘制,maxdrawdown 默认不绘制。 - - ``fund`` (default: ``None``) + Args: + fund: 如果为 ``None``,会自动检测 broker 的实际模式(fundmode - + True/False),以决定 drawdown 基于总净资产 value 还是 fund + value。将其设为 ``True`` 或 ``False`` 可指定具体行为。 - If ``None`` the actual mode of the broker (fundmode - True/False) will - be autodetected to decide if the returns are based on the total net - asset value or on the fund value. See ``set_fundmode`` in the broker - documentation + Returns: + None: observer 通过 ``drawdown`` 和 ``maxdrawdown`` lines 暴露当前值。 - Set it to ``True`` or ``False`` for a specific behavior + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addobserver(DrawDown) ''' _stclock = True @@ -59,15 +62,25 @@ def __init__(self): **kwargs) def next(self): - self.lines.drawdown[0] = self._dd.rets.drawdown # update drawdown - self.lines.maxdrawdown[0] = self._dd.rets.max.drawdown # update max + self.lines.drawdown[0] = self._dd.rets.drawdown # 更新 drawdown + self.lines.maxdrawdown[0] = self._dd.rets.max.drawdown # 更新最大值 class DrawDownLength(Observer): - '''This observer keeps track of the current drawdown length (plotted) and - the drawdown max length (not plotted) + '''跟踪当前 drawdown length 和最大 drawdown length 的 observer。 - Params: None + 当前 drawdown length 会被绘制,最大 length 默认不绘制。 + + Args: + 无。 + + Returns: + None: observer 通过 ``len`` 和 ``maxlen`` lines 暴露当前值。 + + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addobserver(DrawDownLength) ''' _stclock = True @@ -81,15 +94,23 @@ def __init__(self): self._dd = self._owner._addanalyzer_slave(bt.analyzers.DrawDown) def next(self): - self.lines.len[0] = self._dd.rets.len # update drawdown length - self.lines.maxlen[0] = self._dd.rets.max.len # update max length + self.lines.len[0] = self._dd.rets.len # 更新 drawdown length + self.lines.maxlen[0] = self._dd.rets.max.len # 更新最大 length class DrawDown_Old(Observer): - '''This observer keeps track of the current drawdown level (plotted) and - the maxdrawdown (not plotted) levels + '''旧版 drawdown observer,跟踪当前 drawdown 和 maxdrawdown。 + + Args: + 无。 + + Returns: + None: observer 通过 ``drawdown`` 和 ``maxdrawdown`` lines 暴露当前值。 - Params: None + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addobserver(DrawDown_Old) ''' _stclock = True @@ -108,12 +129,12 @@ def __init__(self): def next(self): value = self._owner.broker.getvalue() - # update the maximum seen peak + # 更新已见到的最大峰值 if value > self.peak: self.peak = value - # calculate the current drawdown + # 计算当前 drawdown self.lines.drawdown[0] = dd = 100.0 * (self.peak - value) / self.peak - # update the maxdrawdown if needed + # 按需更新 maxdrawdown self.lines.maxdrawdown[0] = self.maxdd = max(self.maxdd, dd) diff --git a/backtrader/observers/logreturns.py b/backtrader/observers/logreturns.py index e4e9e7bd3..a7b6742cf 100644 --- a/backtrader/observers/logreturns.py +++ b/backtrader/observers/logreturns.py @@ -29,33 +29,26 @@ class LogReturns(bt.Observer): - '''This observer stores the *log returns* of the strategy or a - - Params: - - - ``timeframe`` (default: ``None``) - If ``None`` then the complete return over the entire backtested period - will be reported - - Pass ``TimeFrame.NoTimeFrame`` to consider the entire dataset with no - time constraints - - - ``compression`` (default: ``None``) - - Only used for sub-day timeframes to for example work on an hourly - timeframe by specifying "TimeFrame.Minutes" and 60 as compression - - - ``fund`` (default: ``None``) - - If ``None`` the actual mode of the broker (fundmode - True/False) will - be autodetected to decide if the returns are based on the total net - asset value or on the fund value. See ``set_fundmode`` in the broker - documentation - - Set it to ``True`` or ``False`` for a specific behavior - - Remember that at any moment of a ``run`` the current values can be checked - by looking at the *lines* by name at index ``0``. + '''存储 strategy 或 data 的 *log returns* 的 observer。 + + Args: + timeframe: 统计使用的 timeframe,默认 ``None``。如果为 ``None``, + 报告整个 backtest period 的完整 return。传入 + ``TimeFrame.NoTimeFrame`` 可在不受时间约束的情况下考虑整个 + dataset。 + compression: timeframe 压缩倍数,默认 ``None``。仅用于日内 + timeframe。 + fund: 如果为 ``None``,会自动检测 broker 的实际模式(fundmode - + True/False),以决定 returns 基于总净资产 value 还是 fund value。 + 将其设为 ``True`` 或 ``False`` 可指定具体行为。 + + Returns: + None: observer 通过 ``logret1`` line 暴露当前值。 + + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addobserver(LogReturns) ''' _stclock = True @@ -83,7 +76,19 @@ def next(self): class LogReturns2(LogReturns): - '''Extends the observer LogReturns to show two instruments''' + '''扩展 ``LogReturns``,用于显示两个 instrument。 + + Args: + 继承 ``LogReturns`` 的参数。 + + Returns: + None: observer 通过 ``logret1`` 和 ``logret2`` lines 暴露当前值。 + + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addobserver(LogReturns2) + ''' lines = ('logret2',) def __init__(self): diff --git a/backtrader/observers/timereturn.py b/backtrader/observers/timereturn.py index 49503d599..4ca6d409c 100644 --- a/backtrader/observers/timereturn.py +++ b/backtrader/observers/timereturn.py @@ -31,33 +31,26 @@ class TimeReturn(Observer): - '''This observer stores the *returns* of the strategy. - - Params: - - - ``timeframe`` (default: ``None``) - If ``None`` then the complete return over the entire backtested period - will be reported - - Pass ``TimeFrame.NoTimeFrame`` to consider the entire dataset with no - time constraints - - - ``compression`` (default: ``None``) - - Only used for sub-day timeframes to for example work on an hourly - timeframe by specifying "TimeFrame.Minutes" and 60 as compression - - - ``fund`` (default: ``None``) - - If ``None`` the actual mode of the broker (fundmode - True/False) will - be autodetected to decide if the returns are based on the total net - asset value or on the fund value. See ``set_fundmode`` in the broker - documentation - - Set it to ``True`` or ``False`` for a specific behavior - - Remember that at any moment of a ``run`` the current values can be checked - by looking at the *lines* by name at index ``0``. + '''存储 strategy *returns* 的 observer。 + + Args: + timeframe: 统计使用的 timeframe,默认 ``None``。如果为 ``None``, + 报告整个 backtest period 的完整 return。传入 + ``TimeFrame.NoTimeFrame`` 可在不受时间约束的情况下考虑整个 + dataset。 + compression: timeframe 压缩倍数,默认 ``None``。仅用于日内 + timeframe。 + fund: 如果为 ``None``,会自动检测 broker 的实际模式(fundmode - + True/False),以决定 returns 基于总净资产 value 还是 fund value。 + 将其设为 ``True`` 或 ``False`` 可指定具体行为。 + + Returns: + None: observer 通过 ``timereturn`` line 暴露当前值。 + + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addobserver(TimeReturn) ''' _stclock = True @@ -74,7 +67,7 @@ class TimeReturn(Observer): def _plotlabel(self): return [ - # Use the final tf/comp values calculated by the return analyzer + # 使用 return analyzer 计算出的最终 tf/comp 值 TimeFrame.getname(self.treturn.timeframe, self.treturn.compression), str(self.treturn.compression) diff --git a/backtrader/observers/trades.py b/backtrader/observers/trades.py index 144cf03fb..a6e5acd0f 100644 --- a/backtrader/observers/trades.py +++ b/backtrader/observers/trades.py @@ -30,18 +30,22 @@ class Trades(Observer): - '''This observer keeps track of full trades and plot the PnL level achieved - when a trade is closed. + '''跟踪完整 trade,并在 trade 关闭时绘制对应 PnL 的 observer。 - A trade is open when a position goes from 0 (or crossing over 0) to X and - is then closed when it goes back to 0 (or crosses over 0 in the opposite - direction) + 当 position 从 0(或穿越 0)变为 X 时,trade 被视为打开;当它回到 0 + (或反方向穿越 0)时,trade 被视为关闭。 - Params: - - ``pnlcomm`` (def: ``True``) + Args: + pnlcomm (bool): 是否显示扣除 commission 后的 net profit/loss,默认 + ``True``。设为 ``False`` 时显示扣除 commission 前的 trade 结果。 - Show net/profit and loss, i.e.: after commission. If set to ``False`` - if will show the result of trades before commission + Returns: + None: observer 通过 ``pnlplus`` 和 ``pnlminus`` lines 暴露当前值。 + + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addobserver(Trades) ''' _stclock = True @@ -108,19 +112,19 @@ class MetaDataTrades(Observer.__class__): def donew(cls, *args, **kwargs): _obj, args, kwargs = super(MetaDataTrades, cls).donew(*args, **kwargs) - # Recreate the lines dynamically + # 动态重新创建 lines if _obj.params.usenames: lnames = tuple(x._name for x in _obj.datas) else: lnames = tuple('data{}'.format(x) for x in range(len(_obj.datas))) - # Generate a new lines class + # 生成新的 lines class linescls = cls.lines._derive(uuid.uuid4().hex, lnames, 0, ()) - # Instantiate lines + # 实例化 lines _obj.lines = linescls() - # Generate plotlines info + # 生成 plotlines 信息 markers = ['o', 'v', '^', '<', '>', '1', '2', '3', '4', '8', 's', 'p', '*', 'h', 'H', '+', 'x', 'D', 'd'] @@ -138,10 +142,23 @@ def donew(cls, *args, **kwargs): uuid.uuid4().hex, plines, [], recurse=True) _obj.plotlines = plotlines() - return _obj, args, kwargs # return the instantiated object and args + return _obj, args, kwargs # 返回已实例化对象和 args class DataTrades(with_metaclass(MetaDataTrades, Observer)): + '''按 data 分别绘制 closed trade PnL 的 observer。 + + Args: + usenames (bool): 是否使用 data 名称作为 line 名称,默认 ``True``。 + + Returns: + None: observer 通过动态生成的 data lines 暴露当前值。 + + --- + >>> import backtrader as bt + >>> cerebro = bt.Cerebro() + >>> cerebro.addobserver(DataTrades) + ''' _stclock = True params = (('usenames', True),) diff --git a/backtrader/order.py b/backtrader/order.py index 4a68346cd..5f2c5ea6a 100644 --- a/backtrader/order.py +++ b/backtrader/order.py @@ -34,29 +34,34 @@ class OrderExecutionBit(object): ''' - Intended to hold information about order execution. A "bit" does not - determine if the order has been fully/partially executed, it just holds - information. - - Member Attributes: - - - dt: datetime (float) execution time - - size: how much was executed - - price: execution price - - closed: how much of the execution closed an existing postion - - opened: how much of the execution opened a new position - - openedvalue: market value of the "opened" part - - closedvalue: market value of the "closed" part - - closedcomm: commission for the "closed" part - - openedcomm: commission for the "opened" part - - - value: market value for the entire bit size - - comm: commission for the entire bit execution - - pnl: pnl generated by this bit (if something was closed) - - - psize: current open position size - - pprice: current open position price - + 保存一次 order execution 的信息。一个 "bit" 不负责判断 order 是否完全或 + 部分执行,它只保存执行信息。 + + 成员属性: + + - dt: datetime (float) 执行时间 + - size: 本次执行的数量 + - price: 执行 price + - closed: 本次执行中用于关闭已有 position 的数量 + - opened: 本次执行中用于打开新 position 的数量 + - openedvalue: "opened" 部分的 market value + - closedvalue: "closed" 部分的 market value + - closedcomm: "closed" 部分的 commission + - openedcomm: "opened" 部分的 commission + + - value: 整个 bit size 的 market value + - comm: 整个 bit execution 的 commission + - pnl: 该 bit 产生的 pnl(如果关闭了某些 position) + + - psize: 当前 open position size + - pprice: 当前 open position price + + --- + 交互示例: + + >>> bit = OrderExecutionBit(size=5, price=100.0, opened=5, openedvalue=500.0) + >>> bit.value, bit.comm, bit.pnl + (500.0, 0.0, 0.0) ''' def __init__(self, @@ -65,6 +70,23 @@ def __init__(self, opened=0, openedvalue=0.0, openedcomm=0.0, pnl=0.0, psize=0, pprice=0.0): + ''' + 创建一个 execution bit。 + + Args: + dt: 执行时间,通常是 float 编码的 datetime。 + size (int): 本次执行 size。 + price (float): 本次执行 price。 + closed (int): 本次执行中关闭已有 position 的数量。 + closedvalue (float): ``closed`` 部分的 value。 + closedcomm (float): ``closed`` 部分的 commission。 + opened (int): 本次执行中打开新 position 的数量。 + openedvalue (float): ``opened`` 部分的 value。 + openedcomm (float): ``opened`` 部分的 commission。 + pnl (float): 本次 execution bit 产生的 pnl。 + psize (int): 执行后当前 open position size。 + pprice (float): 执行后当前 open position price。 + ''' self.dt = dt self.size = size @@ -87,52 +109,72 @@ def __init__(self, class OrderData(object): ''' - Holds actual order data for Creation and Execution. + 保存 order 在 Creation 和 Execution 阶段的实际数据。 - In the case of Creation the request made and in the case of Execution the - actual outcome. + 对 Creation 来说,它保存发出的请求;对 Execution 来说,它保存实际结果。 - Member Attributes: + 成员属性: - - exbits : iterable of OrderExecutionBits for this OrderData + - exbits : 该 OrderData 对应的 OrderExecutionBits iterable - - dt: datetime (float) creation/execution time - - size: requested/executed size - - price: execution price - Note: if no price is given and no pricelimite is given, the closing - price at the time or order creation will be used as reference - - pricelimit: holds pricelimit for StopLimit (which has trigger first) - - trailamount: absolute price distance in trailing stops - - trailpercent: percentage price distance in trailing stops + - dt: datetime (float) 创建/执行时间 + - size: 请求/执行 size + - price: 执行 price + 注意: 如果未给出 price,也未给出 pricelimit,order 创建时的 close + price 会被用作参考 + - pricelimit: 保存 StopLimit 的 pricelimit(它会先触发) + - trailamount: trailing stops 中的绝对 price 距离 + - trailpercent: trailing stops 中的百分比 price 距离 - - value: market value for the entire bit size - - comm: commission for the entire bit execution - - pnl: pnl generated by this bit (if something was closed) - - margin: margin incurred by the Order (if any) + - value: 整个 bit size 的 market value + - comm: 整个 bit execution 的 commission + - pnl: 该 bit 产生的 pnl(如果关闭了某些 position) + - margin: Order 产生的 margin(如果有) - - psize: current open position size - - pprice: current open position price + - psize: 当前 open position size + - pprice: 当前 open position price + --- + 交互示例: + + >>> data = OrderData(size=0, price=0.0, remsize=10) + >>> data.add(dt=1.0, size=5, price=100.0, opened=5, openedvalue=500.0) + >>> data.size, data.price, data.remsize + (5, 100.0, 5) + >>> clone = data.clone() + >>> len(clone.getpending()) + 1 ''' - # According to the docs, collections.deque is thread-safe with appends at - # both ends, there will be no pop (nowhere) and therefore to know which the - # new exbits are two indices are needed. At time of cloning (__copy__) the - # indices can be updated to match the previous end, and the new end + # 根据文档,collections.deque 在两端 append 是线程安全的。这里没有 pop, + # 因此只需要两个索引来判断哪些 exbits 是新的。在 clone (__copy__) 时, + # 索引会更新为前一个 end 和新的 end # (len(exbits) # Example: start 0, 0 -> islice(exbits, 0, 0) -> [] # One added -> copy -> updated 0, 1 -> islice(exbits, 0, 1) -> [1 elem] # Other added -> copy -> updated 1, 2 -> islice(exbits, 1, 2) -> [1 elem] - # "add" and "__copy__" happen always in the same thread (with all current - # implementations) and therefore no append will happen during a copy and - # the len of the exbits can be queried with no concerns about another - # thread making an append and with no need for a lock + # 在所有当前实现中,"add" 和 "__copy__" 总是在同一线程发生,因此 copy + # 期间不会 append,可以安全查询 exbits 的 len,无需担心另一个线程 append, + # 也不需要 lock def __init__(self, dt=None, size=0, price=0.0, pricelimit=0.0, remsize=0, pclose=0.0, trailamount=0.0, trailpercent=0.0): + ''' + 创建 OrderData。 + + Args: + dt: 创建/执行时间,通常是 float 编码的 datetime。 + size (int): 请求或已执行 size。 + price (float): 请求或执行 price。 + pricelimit (float): StopLimit 使用的 limit price。 + remsize (int): 剩余未执行 size。 + pclose (float): order 创建时的 close price。 + trailamount (float): trailing stop 的绝对 price 距离。 + trailpercent (float): trailing stop 的百分比 price 距离。 + ''' self.pclose = pclose - self.exbits = collections.deque() # for historical purposes - self.p1, self.p2 = 0, 0 # indices to pending notifications + self.exbits = collections.deque() # 用于保存历史 execution bits + self.p1, self.p2 = 0, 0 # pending notifications 的索引 self.dt = dt self.size = size @@ -143,11 +185,11 @@ def __init__(self, dt=None, size=0, price=0.0, pricelimit=0.0, remsize=0, self.trailpercent = trailpercent if not pricelimit: - # if no pricelimit is given, use the given price + # 未给出 pricelimit 时,使用给定 price self.pricelimit = self.price if pricelimit and not price: - # price must always be set if pricelimit is set ... + # 如果设置了 pricelimit,则必须始终设置 price self.price = pricelimit self.plimit = pricelimit @@ -179,6 +221,22 @@ def add(self, dt, size, price, opened=0, openedvalue=0.0, openedcomm=0.0, pnl=0.0, psize=0, pprice=0.0): + '''添加一次 execution bit。 + + Args: + dt: 执行时间。 + size (int): 本次执行 size。 + price (float): 本次执行 price。 + closed (int): 本次关闭已有 position 的数量。 + closedvalue (float): ``closed`` 部分的 value。 + closedcomm (float): ``closed`` 部分的 commission。 + opened (int): 本次打开新 position 的数量。 + openedvalue (float): ``opened`` 部分的 value。 + openedcomm (float): ``opened`` 部分的 commission。 + pnl (float): 本次执行产生的 pnl。 + psize (int): 执行后的 position size。 + pprice (float): 执行后的 position price。 + ''' self.addbit( OrderExecutionBit(dt, size, price, @@ -187,7 +245,11 @@ def add(self, dt, size, price, psize, pprice)) def addbit(self, exbit): - # Stores an ExecutionBit and recalculates own values from ExBit + '''保存 ExecutionBit,并基于 ExBit 重新计算自身值。 + + Args: + exbit (OrderExecutionBit): 要加入的 execution bit。 + ''' self.exbits.append(exbit) self.remsize -= exbit.size @@ -204,16 +266,31 @@ def addbit(self, exbit): self.pprice = exbit.pprice def getpending(self): + '''返回 pending execution bits。 + + Returns: + list: 当前 pending 的 execution bits。 + ''' return list(self.iterpending()) def iterpending(self): + '''迭代 pending execution bits。 + + Returns: + itertools.islice: pending execution bits 的 iterator。 + ''' return itertools.islice(self.exbits, self.p1, self.p2) def markpending(self): - # rebuild the indices to mark which exbits are pending in clone + '''重建索引,以标记 clone 中哪些 exbits 处于 pending 状态。''' self.p1, self.p2 = self.p2, len(self.exbits) def clone(self): + '''复制当前 OrderData,并把新增 execution bits 标记为 pending。 + + Returns: + OrderData: 当前对象的浅拷贝,带有更新后的 pending 索引。 + ''' self.markpending() obj = copy(self) return obj @@ -382,19 +459,38 @@ def __init__(self): self.dteos = 0.0 def clone(self): - # status, triggered and executed are the only moving parts in order - # status and triggered are covered by copy - # executed has to be replaced with an intelligent clone of itself + '''复制当前 order。 + + Returns: + OrderBase: 当前 order 的浅拷贝,其中 ``executed`` 会使用自身的 + ``clone`` 结果替换,以保留 pending execution bits。 + ''' + # status、triggered 和 executed 是 order 中仅有的可变部分。 + # status 和 triggered 已由 copy 覆盖,executed 需要替换为自身的智能 clone obj = copy(self) obj.executed = self.executed.clone() - return obj # status could change in next to completed + return obj # status 可能在下一步变成 completed def getstatusname(self, status=None): - '''Returns the name for a given status or the one of the order''' + '''返回给定 status 的名称,或当前 order 的 status 名称。 + + Args: + status (int): 可选 status。未提供时使用当前 order status。 + + Returns: + str: status 名称。 + ''' return self.Status[self.status if status is None else status] def getordername(self, exectype=None): - '''Returns the name for a given exectype or the one of the order''' + '''返回给定 exectype 的名称,或当前 order 的 exectype 名称。 + + Args: + exectype (int): 可选 execution type。未提供时使用当前 order exectype。 + + Returns: + str: execution type 名称。 + ''' return self.ExecTypes[self.exectype if exectype is None else exectype] @classmethod @@ -402,7 +498,14 @@ def ExecType(cls, exectype): return getattr(cls, exectype) def ordtypename(self, ordtype=None): - '''Returns the name for a given ordtype or the one of the order''' + '''返回给定 ordtype 的名称,或当前 order 的 ordtype 名称。 + + Args: + ordtype (int): 可选 order type。未提供时使用当前 order ordtype。 + + Returns: + str: order type 名称。 + ''' return self.OrdTypes[self.ordtype if ordtype is None else ordtype] def active(self): @@ -412,19 +515,28 @@ def activate(self): self._active = True def alive(self): - '''Returns True if the order is in a status in which it can still be - executed + '''判断 order 是否仍处于可执行状态。 + + Returns: + bool: 如果 order status 为 Created、Submitted、Partial 或 Accepted, + 返回 ``True``。 ''' return self.status in [Order.Created, Order.Submitted, Order.Partial, Order.Accepted] def addcomminfo(self, comminfo): - '''Stores a CommInfo scheme associated with the asset''' + '''保存与资产关联的 CommInfo scheme。 + + Args: + comminfo: 要关联到 order 的 CommInfo 对象。 + ''' self.comminfo = comminfo def addinfo(self, **kwargs): - '''Add the keys, values of kwargs to the internal info dictionary to - hold custom information in the order + '''将 ``kwargs`` 中的 key/value 加入内部 info dictionary。 + + Args: + **kwargs: 要保存在 order 中的自定义信息。 ''' for key, val in iteritems(kwargs): self.info[key] = val @@ -436,40 +548,70 @@ def __ne__(self, other): return self.ref != other.ref def isbuy(self): - '''Returns True if the order is a Buy order''' + '''判断 order 是否为 Buy order。 + + Returns: + bool: 如果 order 是 Buy order,返回 ``True``。 + ''' return self.ordtype == self.Buy def issell(self): - '''Returns True if the order is a Sell order''' + '''判断 order 是否为 Sell order。 + + Returns: + bool: 如果 order 是 Sell order,返回 ``True``。 + ''' return self.ordtype == self.Sell def setposition(self, position): - '''Receives the current position for the asset and stotres it''' + '''接收并保存该资产的当前 position。 + + Args: + position: 当前资产 position。 + ''' self.position = position def submit(self, broker=None): - '''Marks an order as submitted and stores the broker to which it was - submitted''' + '''将 order 标记为 submitted,并保存提交到的 broker。 + + Args: + broker: 接收该 order 的 broker。 + ''' self.status = Order.Submitted self.broker = broker self.plen = len(self.data) def accept(self, broker=None): - '''Marks an order as accepted''' + '''将 order 标记为 accepted。 + + Args: + broker: accept 该 order 的 broker。 + ''' self.status = Order.Accepted self.broker = broker def brokerstatus(self): - '''Tries to retrieve the status from the broker in which the order is. + '''尝试从 order 所属 broker 获取 status。 - Defaults to last known status if no broker is associated''' + Returns: + int: broker 返回的 order status;如果未关联 broker,则返回最后已知 + status。 + ''' if self.broker: return self.broker.orderstatus(self) return self.status def reject(self, broker=None): - '''Marks an order as rejected''' + '''将 order 标记为 rejected。 + + Args: + broker: reject 该 order 的 broker。 + + Returns: + bool: 如果本次成功标记为 rejected,返回 ``True``;如果已经是 + rejected,返回 ``False``。 + ''' if self.status == Order.Rejected: return False @@ -480,23 +622,23 @@ def reject(self, broker=None): return True def cancel(self): - '''Marks an order as cancelled''' + '''将 order 标记为 cancelled。''' self.status = Order.Canceled if not self.p.simulated: self.executed.dt = self.data.datetime[0] def margin(self): - '''Marks an order as having met a margin call''' + '''将 order 标记为遇到 margin call。''' self.status = Order.Margin if not self.p.simulated: self.executed.dt = self.data.datetime[0] def completed(self): - '''Marks an order as completely filled''' + '''将 order 标记为完全成交。''' self.status = self.Completed def partial(self): - '''Marks an order as partially filled''' + '''将 order 标记为部分成交。''' self.status = self.Partial def execute(self, dt, size, price, @@ -505,7 +647,23 @@ def execute(self, dt, size, price, margin, pnl, psize, pprice): - '''Receives data execution input and stores it''' + '''接收并保存 data execution 输入。 + + Args: + dt: 执行时间。 + size (int): 执行 size。 + price (float): 执行 price。 + closed (int): 用于关闭已有 position 的数量。 + closedvalue (float): ``closed`` 部分的 value。 + closedcomm (float): ``closed`` 部分的 commission。 + opened (int): 用于打开新 position 的数量。 + openedvalue (float): ``opened`` 部分的 value。 + openedcomm (float): ``opened`` 部分的 commission。 + margin (float): 本次执行产生的 margin。 + pnl (float): 本次执行产生的 pnl。 + psize (int): 执行后 position size。 + pprice (float): 执行后 position price。 + ''' if not size: return @@ -517,7 +675,11 @@ def execute(self, dt, size, price, self.executed.margin = margin def expire(self): - '''Marks an order as expired. Returns True if it worked''' + '''将 order 标记为 expired。 + + Returns: + bool: 如果成功标记为 expired,返回 ``True``。 + ''' self.status = self.Expired return True @@ -527,40 +689,37 @@ def trailadjust(self, price): class Order(OrderBase): ''' - Class which holds creation/execution data and type of oder. + 保存 order 创建/执行数据以及 order type 的类。 - The order may have the following status: + order 可能有以下 status: - - Submitted: sent to the broker and awaiting confirmation - - Accepted: accepted by the broker - - Partial: partially executed - - Completed: fully exexcuted - - Canceled/Cancelled: canceled by the user - - Expired: expired - - Margin: not enough cash to execute the order. - - Rejected: Rejected by the broker + - Submitted: 已发送给 broker,等待确认 + - Accepted: 已被 broker 接受 + - Partial: 部分成交 + - Completed: 完全成交 + - Canceled/Cancelled: 被用户取消 + - Expired: 已过期 + - Margin: cash 不足,无法执行 order + - Rejected: 被 broker 拒绝 - This can happen during order submission (and therefore the order will - not reach the Accepted status) or before execution with each new bar - price because cash has been drawn by other sources (future-like - instruments may have reduced the cash or orders orders may have been - executed) + 这可能发生在 order 提交期间(因此 order 不会进入 Accepted status),也 + 可能发生在每个新 bar price 执行前,因为 cash 已被其他来源占用 + (future-like instruments 可能降低了 cash,或其他 orders 已经执行)。 - Member Attributes: + 成员属性: - - ref: unique order identifier - - created: OrderData holding creation data - - executed: OrderData holding execution data + - ref: 唯一 order 标识符 + - created: 保存 creation data 的 OrderData + - executed: 保存 execution data 的 OrderData - - info: custom information passed over method :func:`addinfo`. It is kept - in the form of an OrderedDict which has been subclassed, so that keys - can also be specified using '.' notation + - info: 通过 :func:`addinfo` 方法传入的自定义信息。它以 OrderedDict + 子类形式保存,因此 key 也可以用 ``.`` 访问语法指定 - User Methods: + 用户方法: - - isbuy(): returns bool indicating if the order buys - - issell(): returns bool indicating if the order sells - - alive(): returns bool if order is in status Partial or Accepted + - isbuy(): 返回 bool,表示 order 是否 buy + - issell(): 返回 bool,表示 order 是否 sell + - alive(): 返回 bool,表示 order 是否处于 Partial 或 Accepted status ''' def execute(self, dt, size, price, diff --git a/backtrader/plot/__init__.py b/backtrader/plot/__init__.py index d949c7fa3..4c352c515 100644 --- a/backtrader/plot/__init__.py +++ b/backtrader/plot/__init__.py @@ -28,14 +28,13 @@ import matplotlib except ImportError: raise ImportError( - 'Matplotlib seems to be missing. Needed for plotting support') + '缺少 Matplotlib;绘图支持需要该依赖') else: touse = 'TKAgg' if sys.platform != 'darwin' else 'MacOSX' try: matplotlib.use(touse) except: - # if another backend has already been loaded, an exception will be - # generated and this can be skipped + # 如果已经加载了另一个 backend,会生成异常;此处可以跳过 pass diff --git a/backtrader/plot/finance.py b/backtrader/plot/finance.py index 9aea06085..e6f7c54aa 100644 --- a/backtrader/plot/finance.py +++ b/backtrader/plot/finance.py @@ -50,12 +50,12 @@ def __init__(self, filldown=True, **kwargs): - # Manager up/down bar colors + # 管理 up/down bar 颜色 r, g, b = mcolors.colorConverter.to_rgb(colorup) self.colorup = r, g, b, alpha r, g, b = mcolors.colorConverter.to_rgb(colordown) self.colordown = r, g, b, alpha - # Manage the edge up/down colors for the bars + # 管理 bar 的 up/down edge 颜色 if edgeup: r, g, b = mcolors.colorConverter.to_rgb(edgeup) self.edgeup = ((r, g, b, alpha),) @@ -68,7 +68,7 @@ def __init__(self, else: self.edgedown = shade_color(self.colordown, edgeshading) - # Manage the up/down tick colors + # 管理 up/down tick 颜色 if tickup: r, g, b = mcolors.colorConverter.to_rgb(tickup) self.tickup = ((r, g, b, alpha),) @@ -88,15 +88,15 @@ def __init__(self, fillup=fillup, filldown=filldown, **kwargs) - # add collections to the axis and return them + # 将 collections 添加到 axis ax.add_collection(self.tickcol) ax.add_collection(self.barcol) - # Update the axis + # 更新 axis ax.update_datalim(((0, min(lows)), (len(opens), max(highs)))) ax.autoscale_view() - # Add self as legend handler for this object + # 将自身注册为该对象的 legend handler mlegend.Legend.update_default_handler_map({self.barcol: self}) def legend_artist(self, legend, orig_handle, fontsize, handlebox): @@ -105,7 +105,7 @@ def legend_artist(self, legend, orig_handle, fontsize, handlebox): width = handlebox.width / len(self.legend_opens) height = handlebox.height - # Generate the x axis coordinates (handlebox based) + # 生成 x 轴坐标(基于 handlebox) xs = [x0 + width * (i + 0.5) for i in range(len(self.legend_opens))] barcol, tickcol = self.barcollection( @@ -131,7 +131,7 @@ def barcollection(self, fillup=True, filldown=True, **kwargs): - # Prepack different zips of the series values + # 预打包 series value 的不同 zip 组合 oc = lambda: zip(opens, closes) # NOQA: E731 xoc = lambda: zip(xs, opens, closes) # NOQA: E731 iohlc = lambda: zip(xs, opens, highs, lows, closes) # NOQA: E731 @@ -150,7 +150,7 @@ def barcollection(self, delta = width / 2 - edgeadjust def barbox(i, open, close): - # delta seen as closure + # delta 作为 closure 使用 left, right = i - delta, i + delta open = open * scaling + bot close = close * scaling + bot @@ -176,12 +176,12 @@ def tdown(i, open, low, close): tickrangesdown = [tdown(i, o, l, c) for i, o, h, l, c in iohlc()] - # Extra variables for the collections - useaa = 0, # use tuple here - lw = 0.5, # and here + # collections 使用的附加变量 + useaa = 0, # 此处使用 tuple + lw = 0.5, # 此处同样使用 tuple tlw = tickwidth, - # Bar collection for the candles + # candle 使用的 bar collection barcol = mcol.PolyCollection( barareas, facecolors=colors, @@ -191,12 +191,11 @@ def tdown(i, open, low, close): label=label, **kwargs) - # LineCollections have a higher zorder than PolyCollections - # to ensure the edges of the bars are not overwriten by the Lines - # we need to put the bars slightly over the LineCollections + # LineCollections 的 zorder 高于 PolyCollections。 + # 为避免 bar edge 被 Lines 覆盖,需要让 bars 略高于 LineCollections。 kwargs['zorder'] = barcol.get_zorder() * 0.9999 - # Up/down ticks from the body + # body 上下两侧的 ticks tickcol = mcol.LineCollection( tickrangesup + tickrangesdown, colors=tickcolors, @@ -204,7 +203,7 @@ def tdown(i, open, low, close): antialiaseds=useaa, **kwargs) - # return barcol, tickcol + # 返回 barcol, tickcol return barcol, tickcol @@ -234,8 +233,7 @@ def plot_candlestick(ax, filldown, **kwargs) - # Return the collections. the barcol goes first because - # is the larger, has the dominant zorder and defines the legend + # 返回 collections。barcol 放在前面,因为它更大、有主要 zorder,并定义 legend。 return chandler.barcol, chandler.tickcol @@ -252,13 +250,13 @@ def __init__(self, width=1, alpha=1.0, **kwargs): - # Manage the up/down colors + # 管理 up/down 颜色 r, g, b = mcolors.colorConverter.to_rgb(colorup) self.colorup = r, g, b, alpha r, g, b = mcolors.colorConverter.to_rgb(colordown) self.colordown = r, g, b, alpha - # Prepare the edge colors + # 准备 edge 颜色 if not edgeup: self.edgeup = shade_color(self.colorup, edgeshading) else: @@ -280,10 +278,10 @@ def __init__(self, width=width, edgeadjust=edgeadjust, **kwargs) - # add to axes + # 添加到 axes ax.add_collection(self.barcol) - # Add a legend handler for this object + # 为该对象添加 legend handler mlegend.Legend.update_default_handler_map({self.barcol: self}) def legend_artist(self, legend, orig_handle, fontsize, handlebox): @@ -292,7 +290,7 @@ def legend_artist(self, legend, orig_handle, fontsize, handlebox): width = handlebox.width / len(self.legend_vols) height = handlebox.height - # Generate the x axis coordinates (handlebox based) + # 生成 x 轴坐标(基于 handlebox) xs = [x0 + width * (i + 0.5) for i in range(len(self.legend_vols))] barcol = self.barcollection( @@ -310,19 +308,19 @@ def barcollection(self, vscaling=1.0, vbot=0, **kwargs): - # Prepare the data + # 准备数据 openclose = lambda: zip(opens, closes) # NOQA: E731 - # Calculate bars colors + # 计算 bar 颜色 colord = {True: self.colorup, False: self.colordown} colors = [colord[open < close] for open, close in openclose()] edgecolord = {True: self.edgeup, False: self.edgedown} edgecolors = [edgecolord[open < close] for open, close in openclose()] - # bar width to the sides + # bar 向两侧展开的宽度 delta = width / 2 - edgeadjust - # small auxiliary func to return the bar coordinates + # 返回 bar 坐标的小型辅助函数 def volbar(i, v): left, right = i - delta, i + delta v = vbot + v * vscaling @@ -373,7 +371,7 @@ def __init__(self, label='_nolegend', **kwargs): - # Manager up/down bar colors + # 管理 up/down bar 颜色 r, g, b = mcolors.colorConverter.to_rgb(colorup) self.colorup = r, g, b, alpha r, g, b = mcolors.colorConverter.to_rgb(colordown) @@ -389,16 +387,16 @@ def __init__(self, self.opencol = ocol self.closecol = ccol - # add collections to the axis and return them + # 将 collections 添加到 axis ax.add_collection(self.barcol) ax.add_collection(self.opencol) ax.add_collection(self.closecol) - # Update the axis + # 更新 axis ax.update_datalim(((0, min(lows)), (len(opens), max(highs)))) ax.autoscale_view() - # Add self as legend handler for this object + # 将自身注册为该对象的 legend handler mlegend.Legend.update_default_handler_map({self.barcol: self}) def legend_artist(self, legend, orig_handle, fontsize, handlebox): @@ -407,7 +405,7 @@ def legend_artist(self, legend, orig_handle, fontsize, handlebox): width = handlebox.width / len(self.legend_opens) height = handlebox.height - # Generate the x axis coordinates (handlebox based) + # 生成 x 轴坐标(基于 handlebox) xs = [x0 + width * (i + 0.5) for i in range(len(self.legend_opens))] barcol, opencol, closecol = self.barcollection( @@ -434,7 +432,7 @@ def barcollection(self, scaling=1.0, bot=0, **kwargs): - # Prepack different zips of the series values + # 预打包 series value 的不同 zip 组合 ihighlow = lambda: zip(xs, highs, lows) # NOQA: E731 iopen = lambda: zip(xs, opens) # NOQA: E731 iclose = lambda: zip(xs, closes) # NOQA: E731 @@ -443,12 +441,12 @@ def barcollection(self, colord = {True: self.colorup, False: self.colordown} colors = [colord[open < close] for open, close in openclose()] - # Extra variables for the collections + # collections 使用的附加变量 useaa = 0, lw = width, tlw = tickwidth, - # Calculate the barranges + # 计算 bar range def barrange(i, high, low): return (i, low * scaling + bot), (i, high * scaling + bot) @@ -488,7 +486,7 @@ def tickclose(i, close): label='_nolegend', **kwargs) - # return barcol, tickcol + # 返回 barcol, tickcol return barcol, opencol, closecol @@ -528,14 +526,14 @@ def __init__(self, label=label, **kwargs) - # add collections to the axis and return them + # 将 collection 添加到 axis ax.add_line(self.loc) - # Update the axis + # 更新 axis ax.update_datalim(((x[0], min(closes)), (x[-1], max(closes)))) ax.autoscale_view() - # Add self as legend handler for this object + # 将自身注册为该对象的 legend handler mlegend.Legend.update_default_handler_map({self.loc: self}) def legend_artist(self, legend, orig_handle, fontsize, handlebox): @@ -544,7 +542,7 @@ def legend_artist(self, legend, orig_handle, fontsize, handlebox): width = handlebox.width / len(self.legend_closes) height = handlebox.height - # Generate the x axis coordinates (handlebox based) + # 生成 x 轴坐标(基于 handlebox) xs = [x0 + width * (i + 0.5) for i in range(len(self.legend_closes))] linecol, = self.barcollection( @@ -564,7 +562,7 @@ def barcollection(self, scaling=1.0, bot=0, **kwargs): - # Prepack different zips of the series values + # 预打包 series value 的不同 zip 组合 scaled = [close * scaling + bot for close in closes] loc = mlines.Line2D( diff --git a/backtrader/plot/formatters.py b/backtrader/plot/formatters.py index 2406d595c..45d74e2b6 100644 --- a/backtrader/plot/formatters.py +++ b/backtrader/plot/formatters.py @@ -41,7 +41,7 @@ def __init__(self, volmax): self.suffix = self.Suffixes[magnitude] def __call__(self, y, pos=0): - '''Return the label for time x at position pos''' + '''返回 ``pos`` 位置上 volume ``y`` 的 label。''' if y > self.volmax * 1.20: return '' @@ -57,7 +57,7 @@ def __init__(self, dates, fmt='%Y-%m-%d'): self.fmt = fmt def __call__(self, x, pos=0): - '''Return the label for time x at position pos''' + '''返回 ``pos`` 位置上 time ``x`` 的 label。''' ind = int(round(x)) if ind >= self.lendates: ind = self.lendates - 1 @@ -72,7 +72,7 @@ def patch_locator(locator, xdates): def _patched_datalim_to_dt(self): dmin, dmax = self.axis.get_data_interval() - # proxy access to xdates + # 代理访问 xdates dmin, dmax = xdates[int(dmin)], xdates[min(int(dmax), len(xdates) - 1)] a, b = num2date(dmin, self.tz), num2date(dmax, self.tz) @@ -81,12 +81,12 @@ def _patched_datalim_to_dt(self): def _patched_viewlim_to_dt(self): vmin, vmax = self.axis.get_view_interval() - # proxy access to xdates + # 代理访问 xdates vmin, vmax = xdates[int(vmin)], xdates[min(int(vmax), len(xdates) - 1)] a, b = num2date(vmin, self.tz), num2date(vmax, self.tz) return a, b - # patch the instance with a bound method + # 用 bound method patch 实例 bound_datalim = _patched_datalim_to_dt.__get__(locator, locator.__class__) locator.datalim_to_dt = bound_datalim diff --git a/backtrader/plot/locator.py b/backtrader/plot/locator.py index 848e62bb8..e4781b0bf 100644 --- a/backtrader/plot/locator.py +++ b/backtrader/plot/locator.py @@ -126,19 +126,17 @@ def tick_values(self, vmin, vmax): return [bisect.bisect_left(self._dates, x) for x in dtnums] def get_locator(self, dmin, dmax): - 'Pick the best locator based on a distance.' + '根据时间距离选择最佳 locator。' delta = relativedelta(dmax, dmin) tdelta = dmax - dmin - # take absolute difference + # 使用绝对差值 if dmin > dmax: delta = -delta tdelta = -tdelta - # The following uses a mix of calls to relativedelta and timedelta - # methods because there is incomplete overlap in the functionality of - # these similar functions, and it's best to avoid doing our own math - # whenever possible. + # 下面混用 relativedelta 与 timedelta 方法,因为这些相似函数的能力不完全重叠; + # 能复用库方法时避免自己实现日期数学。 numYears = float(delta.years) numMonths = (numYears * MONTHS_PER_YEAR) + delta.months numDays = tdelta.days # Avoids estimates of days/month, days/year @@ -152,42 +150,36 @@ def get_locator(self, dmin, dmax): use_rrule_locator = [True] * 6 + [False] - # Default setting of bymonth, etc. to pass to rrule - # [unused (for year), bymonth, bymonthday, byhour, byminute, - # bysecond, unused (for microseconds)] + # 传给 rrule 的 bymonth 等默认设置: + # [unused(year), bymonth, bymonthday, byhour, byminute, + # bysecond, unused(microseconds)] byranges = [None, 1, 1, 0, 0, 0, None] - usemicro = False # use as flag to avoid raising an exception + usemicro = False # 作为 flag 使用,避免抛出异常 - # Loop over all the frequencies and try to find one that gives at - # least a minticks tick positions. Once this is found, look for - # an interval from an list specific to that frequency that gives no - # more than maxticks tick positions. Also, set up some ranges - # (bymonth, etc.) as appropriate to be passed to rrulewrapper. + # 遍历所有 frequency,找到至少能给出 minticks 个 tick position 的配置。 + # 找到后,再从该 frequency 对应列表中选择不超过 maxticks 的 interval。 + # 同时准备传给 rrulewrapper 的 bymonth 等 range。 for i, (freq, num) in enumerate(zip(self._freqs, nums)): - # If this particular frequency doesn't give enough ticks, continue + # 当前 frequency 不能给出足够 tick 时继续尝试下一种 if num < self.minticks: - # Since we're not using this particular frequency, set - # the corresponding by_ to None so the rrule can act as - # appropriate + # 未使用该 frequency 时,将对应 by_ 设为 None,便于 rrule 正确处理 byranges[i] = None continue - # Find the first available interval that doesn't give too many - # ticks + # 找到第一个不会产生过多 tick 的 interval for interval in self.intervald[freq]: if num <= interval * (self.maxticks[freq] - 1): break else: - # We went through the whole loop without breaking, default to - # the last interval in the list and raise a warning + # 遍历后仍未找到合适 interval,默认使用列表最后一个并发出 warning warnings.warn('AutoDateLocator was unable to pick an ' 'appropriate interval for this date range. ' 'It may be necessary to add an interval value ' "to the AutoDateLocator's intervald dictionary." ' Defaulting to {0}.'.format(interval)) - # Set some parameters as appropriate + # 设置相应参数 self._freq = freq if self._byranges[i] and self.interval_multiples: @@ -196,7 +188,7 @@ def get_locator(self, dmin, dmax): else: byranges[i] = self._byranges[i] - # We found what frequency to use + # 已找到要使用的 frequency break else: if False: @@ -218,18 +210,18 @@ def get_locator(self, dmin, dmax): locator = RRuleLocator(self._dates, rrule, self.tz) else: if usemicro: - interval = 1 # not set because the for else: was met + interval = 1 # 因为进入 for else,尚未设置 interval locator = MicrosecondLocator(interval, tz=self.tz) locator.set_axis(self.axis) try: - # try for matplotlib < 3.6.0 + # 尝试兼容 matplotlib < 3.6.0 locator.set_view_interval(*self.axis.get_view_interval()) locator.set_data_interval(*self.axis.get_data_interval()) except Exception as e: try: - # try for matplotlib >= 3.6.0 + # 尝试兼容 matplotlib >= 3.6.0 self.axis.set_view_interval(*self.axis.get_view_interval()) self.axis.set_data_interval(*self.axis.get_data_interval()) locator.set_axis(self.axis) @@ -245,7 +237,7 @@ def __init__(self, dates, locator, tz=None, defaultfmt='%Y-%m-%d'): super(AutoDateFormatter, self).__init__(locator, tz, defaultfmt) def __call__(self, x, pos=None): - '''Return the label for time x at position pos''' + '''返回 ``pos`` 位置上 time ``x`` 的 label。''' x = int(round(x)) ldates = len(self._dates) if x >= ldates: diff --git a/backtrader/plot/multicursor.py b/backtrader/plot/multicursor.py index a3ea179cf..6badeb9e9 100644 --- a/backtrader/plot/multicursor.py +++ b/backtrader/plot/multicursor.py @@ -49,58 +49,50 @@ # Agreement. # CHANGES -# The original MultiCursor plots all horizontal lines at the same time -# The modified version plots only the horizontal line in the axis in which the -# motion event takes place +# 原始 MultiCursor 会同时绘制所有水平线。 +# 修改后的版本只在 motion event 所在 axis 中绘制对应水平线。 # -# The original MultiCursos uses the ylimit of the las passed axis, to calculate -# the mid point of the axis. which creates a huge distorsion if all axis don't -# have the same y dimensions +# 原始 MultiCursor 使用最后传入 axis 的 ylimit 计算 axis 中点; +# 如果各 axis 的 y 维度不同,会产生明显失真。 # -# The modified version uses the y limits of each axis to calculate the initial -# position of each line avoiding the distorsion +# 修改后的版本使用每个 axis 自身的 y limit 计算各 line 初始位置,从而避免失真。 from ..utils.py3 import zip class Widget(object): - """ - Abstract base class for GUI neutral widgets - """ + """GUI-neutral widget 的抽象基类,用于统一 widget 激活状态和事件过滤。""" drawon = True eventson = True _active = True def set_active(self, active): - """Set whether the widget is active. + """设置 widget 是否处于 active 状态。 """ self._active = active def get_active(self): - """Get whether the widget is active. + """返回 widget 是否处于 active 状态。 """ return self._active - # set_active is overriden by SelectorWidgets. + # set_active 会被 SelectorWidgets 覆盖。 active = property(get_active, lambda self, active: self.set_active(active), - doc="Is the widget active?") + doc="widget 是否处于 active 状态。") def ignore(self, event): - """Return True if event should be ignored. - This method (or a version of it) should be called at the beginning - of any event callback. + """判断 event 是否应被忽略。 + + 该方法(或其变体)应在任何 event callback 开始处调用。 """ return not self.active class MultiCursor(Widget): - """ - Provide a vertical (default) and/or horizontal line cursor shared between - multiple axes. + """在多个 axes 之间共享 vertical/horizontal line cursor。 - For the cursor to remain responsive you much keep a reference to - it. + 为保持 cursor 响应,需要持有该对象引用。 - Example usage:: + 示例用法:: from matplotlib.widgets import MultiCursor from pylab import figure, show, np @@ -171,18 +163,18 @@ def __init__(self, canvas, axes, useblit=True, self.connect() def connect(self): - """connect events""" + """连接 matplotlib events。""" self._cidmotion = self.canvas.mpl_connect('motion_notify_event', self.onmove) self._ciddraw = self.canvas.mpl_connect('draw_event', self.clear) def disconnect(self): - """disconnect events""" + """断开 matplotlib events。""" self.canvas.mpl_disconnect(self._cidmotion) self.canvas.mpl_disconnect(self._ciddraw) def clear(self, event): - """clear the cursor""" + """清除 cursor。""" if self.ignore(event): return if self.useblit: @@ -238,12 +230,11 @@ def _update(self, event): self.canvas.draw_idle() class MultiCursor2(Widget): - """ - Provide a vertical (default) and/or horizontal line cursor shared between - multiple axes. - For the cursor to remain responsive you much keep a reference to - it. - Example usage:: + """在多个 axes 之间共享 vertical/horizontal line cursor。 + + 为保持 cursor 响应,需要持有该对象引用。 + + 示例用法:: from matplotlib.widgets import MultiCursor from pylab import figure, show, np t = np.arange(0.0, 2.0, 0.01) @@ -296,18 +287,18 @@ def __init__(self, canvas, axes, useblit=True, horizOn=False, vertOn=True, self.connect() def connect(self): - """connect events""" + """连接 matplotlib events。""" self._cidmotion = self.canvas.mpl_connect('motion_notify_event', self.onmove) self._ciddraw = self.canvas.mpl_connect('draw_event', self.clear) def disconnect(self): - """disconnect events""" + """断开 matplotlib events。""" self.canvas.mpl_disconnect(self._cidmotion) self.canvas.mpl_disconnect(self._ciddraw) def clear(self, event): - """clear the cursor""" + """清除 cursor。""" if self.ignore(event): return if self.useblit: diff --git a/backtrader/plot/plot.py b/backtrader/plot/plot.py index 6e92e374f..89364dfd0 100644 --- a/backtrader/plot/plot.py +++ b/backtrader/plot/plot.py @@ -48,6 +48,8 @@ class PInfo(object): + '''绘图状态容器,用于在一次 plot 过程中保存 axis、颜色、legend 和 figure 信息。''' + def __init__(self, sch): self.sch = sch self.nrows = 0 @@ -95,6 +97,8 @@ def zordercur(self, ax): class Plot_OldSync(with_metaclass(MetaParams, object)): + '''同步绘图器的基类,用于把 strategy/data/indicator 渲染为 matplotlib figure。''' + params = (('scheme', PlotScheme()),) def __init__(self, **kwargs): @@ -105,6 +109,7 @@ def __init__(self, **kwargs): setattr(self.p.scheme, 'locbgother', 'white') def drawtag(self, ax, x, y, facecolor, edgecolor, alpha=0.9, **kwargs): + '''在 axis 右侧绘制最后 value 的标签。''' txt = ax.text(x, y, '%.2f' % y, va='center', ha='left', fontsize=self.pinf.sch.subtxtsize, @@ -112,12 +117,13 @@ def drawtag(self, ax, x, y, facecolor, edgecolor, alpha=0.9, **kwargs): facecolor=facecolor, edgecolor=edgecolor, alpha=alpha), - # 3.0 is the minimum default for text + # 3.0 是 text 的最小默认值 zorder=self.pinf.zorder[ax] + 3.0, **kwargs) def plot(self, strategy, figid=0, numfigs=1, iplot=True, start=None, end=None, **kwargs): + '''绘制 strategy 的 data、observer 和 indicator。''' # pfillers={}): if not strategy.datas: return @@ -129,7 +135,7 @@ def plot(self, strategy, figid=0, numfigs=1, iplot=True, if 'ipykernel' in sys.modules: matplotlib.use('nbagg') - # this import must not happen before matplotlib.use + # 该 import 不得早于 matplotlib.use import matplotlib.pyplot as mpyplot self.mpyplot = mpyplot @@ -166,7 +172,7 @@ def plot(self, strategy, figid=0, numfigs=1, iplot=True, figs = [] for numfig in range(numfigs): - # prepare a figure + # 准备 figure fig = self.pinf.newfig(figid, numfig, self.mpyplot) figs.append(fig) @@ -185,13 +191,13 @@ def plot(self, strategy, figid=0, numfigs=1, iplot=True, # pfend = bisect.bisect_right(val, self.pinf.pend) # self.pinf.pfillers[key] = val[pfstart:pfend] - # Do the plotting - # Things that go always at the top (observers) + # 执行绘图 + # 始终位于顶部的对象(observers) self.pinf.xdata = self.pinf.x for ptop in self.dplotstop: self.plotind(None, ptop, subinds=self.dplotsover[ptop]) - # Create the rest on a per data basis + # 其余内容按 data 逐个创建 dt0, dt1 = self.pinf.xreal[0], self.pinf.xreal[-1] for data in strategy.datas: if not data.plotinfo.plot: @@ -240,13 +246,13 @@ def plot(self, strategy, figid=0, numfigs=1, iplot=True, self.pinf.cursors.append(cursor) - # Put the subplots as indicated by hspace + # 按 hspace 指示调整 subplots fig.subplots_adjust(hspace=self.pinf.sch.plotdist, top=0.98, left=0.05, bottom=0.05, right=0.95) laxis = list(self.pinf.daxis.values()) - # Find last axis which is not a twinx (date locator fails there) + # 找到最后一个不是 twinx 的 axis(date locator 在 twinx 上会失败) i = -1 while True: lastax = laxis[i] @@ -257,23 +263,22 @@ def plot(self, strategy, figid=0, numfigs=1, iplot=True, self.setlocators(lastax) # place the locators/fmts - # Applying fig.autofmt_xdate if the data axis is the last one - # breaks the presentation of the date labels. why? - # Applying the manual rotation with setp cures the problem - # but the labels from all axis but the last have to be hidden + # 如果 data axis 是最后一个,应用 fig.autofmt_xdate 会破坏 date label 呈现。 + # 使用 setp 手动旋转可以解决问题,但必须隐藏除最后 axis 外的所有 label。 for ax in laxis: self.mpyplot.setp(ax.get_xticklabels(), visible=False) self.mpyplot.setp(lastax.get_xticklabels(), visible=True, rotation=self.pinf.sch.tickrotation) - # Things must be tight along the x axis (to fill both ends) + # x 轴必须 tight,以填满两端 axtight = 'x' if not self.pinf.sch.ytight else 'both' self.mpyplot.autoscale(enable=True, axis=axtight, tight=True) return figs def setlocators(self, ax): + '''为给定 axis 设置 date locator 和 formatter。''' clock = sorted(self.pinf.clock.datas, key=lambda x: (x._timeframe, x._compression))[0] @@ -304,7 +309,7 @@ def setlocators(self, ax): for dax in self.pinf.daxis.values(): dax.fmt_xdata = fordata - # Major locator / formatter + # major locator / formatter locmajor = loc.AutoDateLocator(self.pinf.xreal) ax.xaxis.set_major_locator(locmajor) if self.pinf.sch.fmt_x_ticks is None: @@ -315,7 +320,9 @@ def setlocators(self, ax): ax.xaxis.set_major_formatter(autofmt) def calcrows(self, strategy): - # Calculate the total number of rows + '''计算绘图需要的总行数。''' + + # 计算总行数 rowsmajor = self.pinf.sch.rowsmajor rowsminor = self.pinf.sch.rowsminor nrows = 0 @@ -323,7 +330,7 @@ def calcrows(self, strategy): datasnoplot = 0 for data in strategy.datas: if not data.plotinfo.plot: - # neither data nor indicators nor volume add rows + # data、indicators 和 volume 都不增加行 datasnoplot += 1 self.dplotsup.pop(data, None) self.dplotsdown.pop(data, None) @@ -334,46 +341,47 @@ def calcrows(self, strategy): if pmaster is data: pmaster = None if pmaster is not None: - # data doesn't add a row, but volume may + # data 不增加行,但 volume 可能增加 if self.pinf.sch.volume: nrows += rowsminor else: - # data adds rows, volume may + # data 增加行,volume 也可能增加 nrows += rowsmajor if self.pinf.sch.volume and not self.pinf.sch.voloverlay: nrows += rowsminor if False: - # Datas and volumes + # Datas 和 volumes nrows += (len(strategy.datas) - datasnoplot) * rowsmajor if self.pinf.sch.volume and not self.pinf.sch.voloverlay: nrows += (len(strategy.datas) - datasnoplot) * rowsminor - # top indicators/observers + # 顶部 indicators/observers nrows += len(self.dplotstop) * rowsminor - # indicators above datas + # data 上方的 indicators nrows += sum(len(v) for v in self.dplotsup.values()) nrows += sum(len(v) for v in self.dplotsdown.values()) self.pinf.nrows = nrows def newaxis(self, obj, rowspan): + '''创建新的 subplot axis,并登记到绘图状态中。''' ax = self.mpyplot.subplot2grid( (self.pinf.nrows, 1), (self.pinf.row, 0), rowspan=rowspan, sharex=self.pinf.sharex) - # update the sharex information if not available + # 如果 sharex 信息尚不可用,则更新 if self.pinf.sharex is None: self.pinf.sharex = ax - # update the row index with the taken rows + # 根据已占用行数更新 row index self.pinf.row += rowspan - # save the mapping indicator - axis and return + # 保存 indicator - axis 映射并返回 self.pinf.daxis[obj] = ax - # Activate grid in all axes if requested + # 如有请求,在所有 axes 中启用 grid ax.yaxis.tick_right() ax.grid(self.pinf.sch.grid, which='both') @@ -382,25 +390,26 @@ def newaxis(self, obj, rowspan): def plotind(self, iref, ind, subinds=None, upinds=None, downinds=None, masterax=None): + '''绘制 indicator 及其上下/嵌套 subindicator。''' sch = self.p.scheme - # check subind + # 检查 subind subinds = subinds or [] upinds = upinds or [] downinds = downinds or [] - # plot subindicators on self with independent axis above + # 在自身上方用独立 axis 绘制 subindicators for upind in upinds: self.plotind(iref, upind) - # Get an axis for this plot + # 获取本次绘图使用的 axis ax = masterax or self.newaxis(ind, rowspan=self.pinf.sch.rowsminor) indlabel = ind.plotlabel() - # Scan lines quickly to find out if some lines have to be skipped for - # legend (because matplotlib reorders the legend) + # 快速扫描 lines,判断是否有 line 需要从 legend 中跳过 + # (因为 matplotlib 会重新排序 legend) toskip = 0 for lineidx in range(ind.size()): line = ind.lines[lineidx] @@ -431,19 +440,19 @@ def plotind(self, iref, ind, if lineplotinfo._get('_plotskip', False): continue - # Legend label only when plotting 1st line + # 仅绘制第 1 条 line 时添加 legend label if masterax and not ind.plotinfo.plotlinelabels: label = indlabel * (not toskip) or '_nolegend' else: label = (indlabel + '\n') * (not toskip) label += lineplotinfo._get('_name', '') or linealias - toskip -= 1 # one line less until legend can be added + toskip -= 1 # 距离可添加 legend 又少一条 line - # plot data + # 绘制 data lplot = line.plotrange(self.pinf.xstart, self.pinf.xend) - # Global and generic for indicator + # indicator 的全局/通用逻辑 if self.pinf.sch.linevalues and ind.plotinfo.plotlinevalues: plotlinevalue = lineplotinfo._get('_plotvalue', True) if plotlinevalue and not math.isnan(lplot[-1]): @@ -467,11 +476,11 @@ def plotind(self, iref, ind, xdata, lplotarray = self.pinf.xdata, lplot if lineplotinfo._get('_skipnan', False): - # Get the full array and a mask to skipnan + # 获取完整 array 和用于 skipnan 的 mask lplotarray = np.array(lplot) lplotmask = np.isfinite(lplotarray) - # Get both the axis and the data masked + # 对 axis 和 data 同时应用 mask lplotarray = lplotarray[lplotmask] xdata = np.array(xdata)[lplotmask] @@ -479,7 +488,7 @@ def plotind(self, iref, ind, try: plottedline = plottedline[0] except: - # Possibly a container of artists (when plotting bars) + # 绘制 bars 时可能是 artists 容器 pass self.pinf.zorder[ax] = plottedline.get_zorder() @@ -488,7 +497,7 @@ def plotind(self, iref, ind, if self.pinf.sch.valuetags and vtags: linetag = lineplotinfo._get('_plotvaluetag', True) if linetag and not math.isnan(lplot[-1]): - # line has valid values, plot a tag for the last value + # line 有有效值,为最后一个 value 绘制 tag self.drawtag(ax, len(self.pinf.xreal), lplot[-1], facecolor=self.pinf.sch.locbgother, edgecolor=self.pinf.color(ax)) @@ -519,19 +528,19 @@ def plotind(self, iref, ind, interpolate=True, **kwargs) - # plot subindicators that were created on self + # 绘制在自身上创建的 subindicators for subind in subinds: self.plotind(iref, subind, subinds=self.dplotsover[subind], masterax=ax) if not masterax: - # adjust margin if requested ... general of particular + # 如有请求,调整 margin;优先使用具体设置,否则使用通用设置 ymargin = ind.plotinfo._get('plotymargin', 0.0) ymargin = max(ymargin, self.pinf.sch.yadjust) if ymargin: ax.margins(y=ymargin) - # Set specific or generic ticks + # 设置特定或通用 ticks yticks = ind.plotinfo._get('plotyticks', []) if not yticks: yticks = ind.plotinfo._get('plotyhlines', []) @@ -542,7 +551,7 @@ def plotind(self, iref, ind, locator = mticker.MaxNLocator(nbins=4, prune='both') ax.yaxis.set_major_locator(locator) - # Set specific hlines if asked to + # 如有请求,设置特定 hlines hlines = ind.plotinfo._get('plothlines', []) if not hlines: hlines = ind.plotinfo._get('plotyhlines', []) @@ -555,33 +564,35 @@ def plotind(self, iref, ind, ind.plotinfo._get('plotlegend', True): handles, labels = ax.get_legend_handles_labels() - # Ensure that we have something to show + # 确保有可展示内容 if labels: - # location can come from the user + # location 可由用户指定 loc = ind.plotinfo.legendloc or self.pinf.sch.legendindloc - # Legend done here to ensure it includes all plots + # 在这里生成 legend,确保包含所有 plots legend = ax.legend(loc=loc, numpoints=1, frameon=False, shadow=False, fancybox=False, prop=self.pinf.prop) # legend.set_title(indlabel, prop=self.pinf.prop) - # hack: if title is set. legend has a Vbox for the labels - # which has a default "center" set + # hack:如果设置 title,legend 会为 labels 创建 Vbox, + # 其默认 align 为 "center" legend._legend_box.align = 'left' - # plot subindicators on self with independent axis below + # 在自身下方用独立 axis 绘制 subindicators for downind in downinds: self.plotind(iref, downind) def plotvolume(self, data, opens, highs, lows, closes, volumes, label): + '''绘制 volume,并按配置决定 overlay 或独立 subplot。''' + pmaster = data.plotinfo.plotmaster if pmaster is data: pmaster = None voloverlay = (self.pinf.sch.voloverlay and pmaster is None) - # if sefl.pinf.sch.voloverlay: + # if self.pinf.sch.voloverlay: if voloverlay: rowspan = self.pinf.sch.rowsmajor else: @@ -598,7 +609,7 @@ def plotvolume(self, data, opens, highs, lows, closes, volumes, label): maxvol = volylim = max(volumes) if maxvol: - # Plot the volume (no matter if as overlay or standalone) + # 绘制 volume(无论 overlay 还是 standalone) vollabel = label volplot, = plot_volume(ax, self.pinf.xdata, opens, closes, volumes, colorup=self.pinf.sch.volup, @@ -609,21 +620,21 @@ def plotvolume(self, data, opens, highs, lows, closes, volumes, label): prune = 'both' # if self.pinf.sch.voloverlay: if voloverlay: - # store for a potential plot over it + # 为后续可能绘制在其上方的 plot 保存设置 nbins = int(nbins / self.pinf.sch.volscaling) prune = None volylim /= self.pinf.sch.volscaling ax.set_ylim(0, volylim, auto=True) else: - # plot a legend + # 绘制 legend handles, labels = ax.get_legend_handles_labels() if handles: - # location can come from the user + # location 可由用户指定 loc = data.plotinfo.legendloc or self.pinf.sch.legendindloc - # Legend done here to ensure it includes all plots + # 在这里生成 legend,确保包含所有 plots legend = ax.legend(loc=loc, numpoints=1, frameon=False, shadow=False, fancybox=False, @@ -640,6 +651,8 @@ def plotvolume(self, data, opens, highs, lows, closes, volumes, label): return volplot def plotdata(self, data, indicators): + '''绘制单个 data feed 及其关联 indicators。''' + for ind in indicators: upinds = self.dplotsup[ind] for upind in upinds: @@ -722,7 +735,7 @@ def plotdata(self, data, indicators): filldown=self.pinf.sch.bardownfill) elif self.pinf.sch.style.startswith('bar') or True: - # final default option -- should be "else" + # 最终默认选项;理论上应为 "else" plotted = plot_ohlc( ax, self.pinf.xdata, opens, highs, lows, closes, colorup=self.pinf.sch.barup, @@ -731,7 +744,7 @@ def plotdata(self, data, indicators): self.pinf.zorder[ax] = plotted[0].get_zorder() - # Code to place a label at the right hand side with the last value + # 在右侧放置最后 value 标签的代码 vtags = data.plotinfo._get('plotvaluetags', True) if self.pinf.sch.valuetags and vtags: self.drawtag(ax, len(self.pinf.xreal), closes[-1], @@ -739,7 +752,7 @@ def plotdata(self, data, indicators): edgecolor=self.pinf.sch.loc) ax.yaxis.set_major_locator(mticker.MaxNLocator(prune='both')) - # make sure "over" indicators do not change our scale + # 确保 "over" indicators 不改变当前 scale if data.plotinfo._get('plotylimited', True): if axdatamaster is None: ax.set_ylim(ax.get_ylim()) @@ -750,9 +763,9 @@ def plotdata(self, data, indicators): self.plotvolume( data, opens, highs, lows, closes, volumes, vollabel) else: - # Prepare overlay scaling/pushup or manage own axis + # 准备 overlay scaling/pushup,或管理自身 axis if self.pinf.sch.volpushup: - # push up overlaid axis by lowering the bottom limit + # 通过降低 bottom limit 将 overlay axis 向上推 axbot, axtop = ax.get_ylim() axbot *= (1.0 - self.pinf.sch.volpushup) ax.set_ylim(axbot, axtop) @@ -763,15 +776,15 @@ def plotdata(self, data, indicators): handles, labels = ax.get_legend_handles_labels() a = axdatamaster or ax if handles: - # put data and volume legend entries in the 1st positions - # because they are "collections" they are considered after Line2D - # for the legend entries, which is not our desire + # 将 data 和 volume legend entries 放到最前面。 + # 因为它们是 "collections",在 legend entries 中会被排到 Line2D 后面, + # 这不是预期顺序。 # if self.pinf.sch.volume and self.pinf.sch.voloverlay: ai = self.pinf.legpos[a] if self.pinf.sch.volume and voloverlay: if volplot: - # even if volume plot was requested, there may be no volume + # 即使请求了 volume plot,也可能没有 volume labels.insert(ai, vollabel) handles.insert(ai, volplot) @@ -799,8 +812,8 @@ def plotdata(self, data, indicators): fancybox=False, prop=self.pinf.prop, numpoints=1, ncol=1) - # hack: if title is set. legend has a Vbox for the labels - # which has a default "center" set + # hack:如果设置 title,legend 会为 labels 创建 Vbox, + # 其默认 align 为 "center" legend._legend_box.align = 'left' for ind in indicators: @@ -818,21 +831,25 @@ def plotdata(self, data, indicators): a.set_yscale('log') def show(self): + '''显示当前 matplotlib plot。''' self.mpyplot.show() def savefig(self, fig, filename, width=16, height=9, dpi=300, tight=True): + '''保存 figure 到文件。''' fig.set_size_inches(width, height) bbox_inches = 'tight' * tight or None fig.savefig(filename, dpi=dpi, bbox_inches=bbox_inches) def sortdataindicators(self, strategy): - # These lists/dictionaries hold the subplots that go above each data + '''按绘图位置整理 data、observer 和 indicator。''' + + # 这些 list/dictionary 保存每个 data 上方/下方/叠加的 subplots self.dplotstop = list() self.dplotsup = collections.defaultdict(list) self.dplotsdown = collections.defaultdict(list) self.dplotsover = collections.defaultdict(list) - # Sort observers in the different lists/dictionaries + # 将 observers 分配到不同 list/dictionary for x in strategy.getobservers(): if not x.plotinfo.plot or x.plotinfo.plotskip: continue @@ -843,18 +860,18 @@ def sortdataindicators(self, strategy): key = getattr(x._clock, 'owner', x._clock) self.dplotsover[key].append(x) - # Sort indicators in the different lists/dictionaries + # 将 indicators 分配到不同 list/dictionary for x in strategy.getindicators(): if not hasattr(x, 'plotinfo'): - # no plotting support - so far LineSingle derived classes + # 暂无绘图支持;目前为 LineSingle 派生类 continue if not x.plotinfo.plot or x.plotinfo.plotskip: continue - x._plotinit() # will be plotted ... call its init function + x._plotinit() # 将被绘制,调用其 init function - # support LineSeriesStub which has "owner" to point to the data + # 支持 LineSeriesStub,其 "owner" 指向 data key = getattr(x._clock, 'owner', x._clock) if key is strategy: # a LinesCoupler key = strategy.data diff --git a/backtrader/plot/scheme.py b/backtrader/plot/scheme.py index e6572a4a7..ef3e129fc 100644 --- a/backtrader/plot/scheme.py +++ b/backtrader/plot/scheme.py @@ -75,113 +75,109 @@ class PlotScheme(object): + '''绘图 scheme 配置容器,用于集中保存 plot 样式默认值。''' + def __init__(self): - # to have a tight packing on the chart wether only the x axis or also - # the y axis have (see matplotlib) + # 控制 chart 是否紧凑排列,可只作用于 x 轴,也可同时作用于 y 轴(见 matplotlib) self.ytight = False - # y-margin (top/bottom) for the subcharts. This will not overrule the - # option plotinfo.plotymargin + # subchart 的 y-margin(top/bottom)。不会覆盖 plotinfo.plotymargin 选项 self.yadjust = 0.0 - # Each new line is in z-order below the previous one. change it False - # to have lines paint above the previous line + # 每条新 line 的 z-order 低于上一条。设为 False 时新 line 会绘制在上一条之上 self.zdown = True - # Rotation of the date labes on the x axis + # x 轴 date label 的旋转角度 self.tickrotation = 15 - # How many "subparts" takes a major chart (datas) in the overall chart - # This is proportional to the total number of subcharts + # major chart(datas)在整体 chart 中占多少“subparts” + # 该值相对 subchart 总数成比例 self.rowsmajor = 5 - # How many "subparts" takes a minor chart (indicators/observers) in the - # overall chart. This is proportional to the total number of subcharts - # Together with rowsmajor, this defines a proportion ratio betwen data - # charts and indicators/observers charts + # minor chart(indicators/observers)在整体 chart 中占多少“subparts” + # 该值相对 subchart 总数成比例。 + # 它与 rowsmajor 一起定义 data chart 与 indicator/observer chart 的比例关系 self.rowsminor = 1 - # Distance in between subcharts + # subchart 之间的距离 self.plotdist = 0.0 - # Have a grid in the background of all charts + # 是否在所有 chart 背景中显示 grid self.grid = True - # Default plotstyle for the OHLC bars which (line -> line on close) - # Other options: 'bar' and 'candle' + # OHLC bar 的默认 plotstyle(line -> line on close) + # 其它选项:'bar' 和 'candle' self.style = 'line' - # Default color for the 'line on close' plot + # 'line on close' plot 的默认颜色 self.loc = 'black' - # Default color for a bullish bar/candle (0.75 -> intensity of gray) + # bullish bar/candle 的默认颜色(0.75 -> gray intensity) self.barup = '0.75' - # Default color for a bearish bar/candle + # bearish bar/candle 的默认颜色 self.bardown = 'red' - # Level of transparency to apply to bars/cancles (NOT USED) + # 应用于 bars/candles 的透明度级别(未使用) self.bartrans = 1.0 - # Wether the candlesticks have to be filled or be transparent + # candlestick 是否填充,还是保持透明 self.barupfill = True self.bardownfill = True - # Opacity for the filled candlesticks (1.0 opaque - 0.0 transparent) + # filled candlestick 的不透明度(1.0 opaque - 0.0 transparent) self.baralpha = 1.0 - # Alpha blending for fill areas between lines (_fill_gt and _fill_lt) + # line 之间填充区域的 alpha blending(_fill_gt 和 _fill_lt) self.fillalpha = 0.20 - # Wether to plot volume or not. Note: if the data in question has no - # volume values, volume plotting will be skipped even if this is True + # 是否绘制 volume。注意:如果对应 data 没有 volume 值,即使这里为 True 也会跳过 self.volume = True - # Wether to overlay the volume on the data or use a separate subchart + # volume 是叠加到 data 上,还是使用独立 subchart self.voloverlay = True - # Scaling of the volume to the data when plotting as overlay + # overlay 绘制时 volume 相对 data 的缩放 self.volscaling = 0.33 - # Pushing overlay volume up for better visibiliy. Experimentation - # needed if the volume and data overlap too much + # 将 overlay volume 上推以提升可见性。如果 volume 与 data 重叠过多,需要实验调整 self.volpushup = 0.00 - # Default colour for the volume of a bullish day + # bullish day volume 的默认颜色 self.volup = '#aaaaaa' # 0.66 of gray - # Default colour for the volume of a bearish day + # bearish day volume 的默认颜色 self.voldown = '#cc6073' # (204, 96, 115) - # Transparency to apply to the volume when overlaying + # overlay volume 时应用的透明度 self.voltrans = 0.50 - # Transparency for text labels (NOT USED CURRENTLY) + # text label 的透明度(当前未使用) self.subtxttrans = 0.66 - # Default font text size for labels on the chart + # chart label 的默认 font size self.subtxtsize = 9 - # Transparency for the legend (NOT USED CURRENTLY) + # legend 的透明度(当前未使用) self.legendtrans = 0.25 - # Wether indicators have a leged displaey in their charts + # indicator 是否在其 chart 中显示 legend self.legendind = True - # Location of the legend for indicators (see matplotlib) + # indicator legend 的位置(见 matplotlib) self.legendindloc = 'upper left' - # Location of the legend for datafeeds (see matplotlib) + # datafeed legend 的位置(见 matplotlib) self.legenddataloc = 'upper left' - # Plot the last value of a line after the Object name + # 在 Object 名称后绘制 line 的最后一个 value self.linevalues = True - # Plot a tag at the end of each line with the last value + # 在每条 line 末尾绘制带最后 value 的 tag self.valuetags = True - # Default color for horizontal lines (see plotinfo.plothlines) + # horizontal line 的默认颜色(见 plotinfo.plothlines) self.hlinescolor = '0.66' # shade of gray - # Default style for horizontal lines + # horizontal line 的默认样式 self.hlinesstyle = '--' - # Default width for horizontal lines + # horizontal line 的默认宽度 self.hlineswidth = 1.0 - # Default color scheme: Tableau 10 + # 默认 color scheme: Tableau 10 self.lcolors = tableau10 - # strftime Format string for the display of ticks on the x axis + # x 轴 tick 显示使用的 strftime format string self.fmt_x_ticks = '%Y-%m-%d %H:%M' - # strftime Format string for the display of data points values + # data point value 显示使用的 strftime format string self.fmt_x_data = None def color(self, idx): diff --git a/backtrader/plot/utils.py b/backtrader/plot/utils.py index cb187b26e..9a4c3014b 100644 --- a/backtrader/plot/utils.py +++ b/backtrader/plot/utils.py @@ -28,23 +28,28 @@ def tag_box_style(x0, y0, width, height, mutation_size, mutation_aspect=1): + """根据 box 的位置和尺寸返回包围它的 path。 + + Args: + x0: box 左下角 x 坐标。 + y0: box 左下角 y 坐标。 + width: box 宽度。 + height: box 高度。 + mutation_size: mutation 的参考尺度。 + mutation_aspect: mutation 的 aspect ratio。 + + Returns: + matplotlib.path.Path: 包围 box 的 path。 """ - Given the location and size of the box, return the path of - the box around it. - - *x0*, *y0*, *width*, *height* : location and size of the box - - *mutation_size* : a reference scale for the mutation. - - *aspect_ratio* : aspect-ration for the mutation. - """ - - # note that we are ignoring mutation_aspect. This is okay in general. + # 这里忽略 mutation_aspect;通常这是可接受的。 mypad = 0.2 pad = mutation_size * mypad - # width and height with padding added. + # 加上 padding 后的 width 和 height。 width, height = width + 2.*pad, height + 2.*pad, - # boundary of the padded box + # padded box 的边界 x0, y0 = x0-pad, y0-pad, x1, y1 = x0+width, y0 + height @@ -64,19 +69,15 @@ def tag_box_style(x0, y0, width, height, mutation_size, mutation_aspect=1): def shade_color(color, percent): - """Shade Color - This color utility function allows the user to easily darken or - lighten a color for plotting purposes. - Parameters - ---------- - color : string, list, hexvalue - Any acceptable Matplotlib color value, such as - 'red', 'slategrey', '#FFEE11', (1,0,0) - percent : the amount by which to brighten or darken the color. - Returns - ------- - color : tuple of floats - tuple representing converted rgb values + """调亮或调暗颜色。 + + Args: + color: 任意 Matplotlib 可接受的颜色值,例如 ``'red'``、``'slategrey'``、 + ``'#FFEE11'``、``(1, 0, 0)``。 + percent: 调亮或调暗的百分比;正数变亮,负数变暗。 + + Returns: + tuple: 转换后的 RGB float 值。 """ rgb = mplcolors.colorConverter.to_rgb(color) diff --git a/backtrader/position.py b/backtrader/position.py index f7f2f764f..bab103b0e 100644 --- a/backtrader/position.py +++ b/backtrader/position.py @@ -27,15 +27,25 @@ class Position(object): ''' - Keeps and updates the size and price of a position. The object has no - relationship to any asset. It only keeps size and price. + 保存并更新 position 的 size 和 price。该对象不与任何具体资产 + 绑定,只记录 size 和 price。 - Member Attributes: - - size (int): current size of the position - - price (float): current price of the position + 成员属性: + - size (int): position 当前 size + - price (float): position 当前 price - The Position instances can be tested using len(position) to see if size - is not null + 可以用 len(position) 测试 Position 实例,以判断 size 是否非零 + + --- + 交互示例: + + >>> position = Position(size=10, price=100.0) + >>> position.size, position.price + (10, 100.0) + >>> position.update(size=-4, price=105.0) + (6, 100.0, 0, -4) + >>> bool(position) + True ''' def __str__(self): @@ -51,6 +61,13 @@ def __str__(self): return '\n'.join(items) def __init__(self, size=0, price=0.0): + ''' + 创建一个 position。 + + Args: + size (int): 初始 position size。正数表示 long,负数表示 short。 + price (float): 初始 position price。仅当 ``size`` 非零时生效。 + ''' self.size = size if size: self.price = self.price_orig = price @@ -66,21 +83,42 @@ def __init__(self, size=0, price=0.0): self.updt = None def fix(self, size, price): + ''' + 直接修正 position 的 size 和 price,不计算 open/close 变化。 + + Args: + size (int): 要设置的新 position size。 + price (float): 要设置的新 position price。 + + Returns: + bool: 如果 size 未发生变化则返回 ``True``,否则返回 ``False``。 + ''' oldsize = self.size self.size = size self.price = price return self.size == oldsize def set(self, size, price): + ''' + 设置 position 的 size 和 price,并根据旧 size 计算本次 opened/closed。 + + Args: + size (int): 要设置的新 position size。 + price (float): 要设置的新 position price。 + + Returns: + tuple: ``(size, price, opened, closed)``,分别表示新的 position + size、position price、本次打开/增加的数量,以及本次关闭/减少的数量。 + ''' if self.size > 0: if size > self.size: - self.upopened = size - self.size # new 10 - old 5 -> 5 + self.upopened = size - self.size # 新 10 - 旧 5 -> 5 self.upclosed = 0 else: - # same side min(0, 3) -> 0 / reversal min(0, -3) -> -3 + # 同方向 min(0, 3) -> 0 / 反转 min(0, -3) -> -3 self.upopened = min(0, size) - # same side min(10, 10 - 5) -> 5 - # reversal min(10, 10 - -5) -> min(10, 15) -> 10 + # 同方向 min(10, 10 - 5) -> 5 + # 反转 min(10, 10 - -5) -> min(10, 15) -> 10 self.upclosed = min(self.size, self.size - size) elif self.size < 0: @@ -88,10 +126,10 @@ def set(self, size, price): self.upopened = size - self.size # ex: -5 - -3 -> -2 self.upclosed = 0 else: - # same side max(0, -5) -> 0 / reversal max(0, 5) -> 5 + # 同方向 max(0, -5) -> 0 / 反转 max(0, 5) -> 5 self.upopened = max(0, size) - # same side max(-10, -10 - -5) -> max(-10, -5) -> -5 - # reversal max(-10, -10 - 5) -> max(-10, -15) -> -10 + # 同方向 max(-10, -10 - -5) -> max(-10, -5) -> -5 + # 反转 max(-10, -10 - 5) -> max(-10, -15) -> -10 self.upclosed = max(self.size, self.size - size) else: # self.size == 0 @@ -116,87 +154,107 @@ def __bool__(self): __nonzero__ = __bool__ def clone(self): + ''' + 复制当前 position。 + + Returns: + Position: 一个带有相同 size 和 price 的新 Position 实例。 + ''' return Position(size=self.size, price=self.price) def pseudoupdate(self, size, price): + ''' + 在副本上模拟一次 update,不修改当前 position。 + + Args: + size (int): 模拟更新的 position size 变化量。 + price (float): 模拟更新使用的 price。 + + Returns: + tuple: 与 ``update`` 相同,返回模拟后的 + ``(size, price, opened, closed)``。 + ''' return Position(self.size, self.price).update(size, price) def update(self, size, price, dt=None): ''' - Updates the current position and returns the updated size, price and - units used to open/close a position + 更新当前 position,并返回更新后的 size、price,以及用于 + open/close position 的单位数 Args: - size (int): amount to update the position size - size < 0: A sell operation has taken place - size > 0: A buy operation has taken place + size (int): 用于更新 position size 的数量 + size < 0: 发生了一次 sell 操作 + size > 0: 发生了一次 buy 操作 price (float): - Must always be positive to ensure consistency + 必须始终为正数,以确保一致性 + + dt (datetime.datetime): 可选的 datetime 标记,会记录到 position 上。 Returns: - A tuple (non-named) contaning - size - new position size - Simply the sum of the existing size plus the "size" argument - price - new position price - If a position is increased the new average price will be - returned - If a position is reduced the price of the remaining size - does not change - If a position is closed the price is nullified - If a position is reversed the price is the price given as - argument - opened - amount of contracts from argument "size" that were used - to open/increase a position. - A position can be opened from 0 or can be a reversal. - If a reversal is performed then opened is less than "size", - because part of "size" will have been used to close the - existing position - closed - amount of units from arguments "size" that were used to - close/reduce a position - - Both opened and closed carry the same sign as the "size" argument - because they refer to a part of the "size" argument + tuple: 一个非命名 tuple,包含 + - size: 新的 position size,即已有 size 与 ``size`` 参数相加后的结果 + - price: 新的 position price。如果 position 增加,返回新的平均 + price;如果 position 减少,剩余 size 的 price 不变;如果 + position 关闭,price 归零;如果 position 反转,price 为参数 + 中给出的 price + - opened: ``size`` 参数中用于 open/increase position 的合约数量。 + position 可以从 0 打开,也可以由反转产生。如果发生反转, + opened 会小于 ``size``,因为 ``size`` 的一部分已被用于 close + 现有 position + - closed: ``size`` 参数中用于 close/reduce position 的单位数 + + opened 和 closed 都与 "size" 参数保持相同符号,因为它们引用的 + 都是 "size" 参数的一部分 + + --- + 交互示例: + + >>> position = Position(size=10, price=100.0) + >>> position.update(size=5, price=110.0) + (15, 103.33333333333333, 5, 0) + >>> position.update(size=-20, price=90.0) + (-5, 90.0, -5, -15) ''' - self.datetime = dt # record datetime update (datetime.datetime) + self.datetime = dt # 记录 datetime 更新 (datetime.datetime) self.price_orig = self.price oldsize = self.size self.size += size if not self.size: - # Update closed existing position + # 更新后关闭了已有 position opened, closed = 0, size self.price = 0.0 elif not oldsize: - # Update opened a position from 0 + # 更新后从 0 打开了 position opened, closed = size, 0 self.price = price - elif oldsize > 0: # existing "long" position updated + elif oldsize > 0: # 已有 "long" position 被更新 - if size > 0: # increased position + if size > 0: # 增加 position opened, closed = size, 0 self.price = (self.price * oldsize + size * price) / self.size - elif self.size > 0: # reduced position + elif self.size > 0: # 减少 position opened, closed = 0, size # self.price = self.price - else: # self.size < 0 # reversed position form plus to minus + else: # self.size < 0 # position 从正数反转为负数 opened, closed = self.size, -oldsize self.price = price - else: # oldsize < 0 - existing short position updated + else: # oldsize < 0 - 已有 short position 被更新 - if size < 0: # increased position + if size < 0: # 增加 position opened, closed = size, 0 self.price = (self.price * oldsize + size * price) / self.size - elif self.size < 0: # reduced position + elif self.size < 0: # 减少 position opened, closed = 0, size # self.price = self.price - else: # self.size > 0 - reversed position from minus to plus + else: # self.size > 0 - position 从负数反转为正数 opened, closed = self.size, -oldsize self.price = price diff --git a/backtrader/resamplerfilter.py b/backtrader/resamplerfilter.py index 73257fd30..6ab0d1307 100644 --- a/backtrader/resamplerfilter.py +++ b/backtrader/resamplerfilter.py @@ -31,23 +31,21 @@ class DTFaker(object): - # This will only be used for data sources which at some point in time - # return None from _load to indicate that a check of the resampler and/or - # notification queue is needed - # This is meant (at least initially) for real-time feeds, because those are - # the ones in need of events like the ones described above. - # These data sources should also be producing ``utc`` time directly because - # the real-time feed is (more often than not) timestamped and utc provides - # a universal reference - # That's why below the timestamp is chosen in UTC and passed directly to - # date2num to avoid a localization. But it is extracted from data.num2date - # to ensure the returned datetime object is localized according to the - # expected output by the user (local timezone or any specified) + '''datetime 代理对象,用于 resampler 在 data 未推进时执行时间检查。''' + + # 仅用于某些 data source:它们会在某些时刻从 _load 返回 None, + # 表示需要检查 resampler 和/或 notification queue。 + # 这最初面向 real-time feed,因为这类 feed 需要上述 event。 + # 这些 data source 也应直接产出 ``utc`` 时间,因为 real-time feed 通常带 timestamp, + # 而 utc 可提供通用参考。 + # 因此下面选择 UTC timestamp 并直接传给 date2num,以避免 localization。 + # 但 datetime object 仍从 data.num2date 提取,以确保返回值按用户期望输出 + # (本地 timezone 或指定 timezone)完成 localized。 def __init__(self, data, forcedata=None): self.data = data - # Aliases + # 别名 self.datetime = self self.p = self @@ -65,7 +63,7 @@ def __len__(self): return len(self.data) def __call__(self, idx=0): - return self._dtime # simulates data.datetime.datetime() + return self._dtime # 模拟 data.datetime.datetime() def datetime(self, idx=0): return self._dtime @@ -94,6 +92,8 @@ def _getnexteos(self): class _BaseResampler(with_metaclass(metabase.MetaParams, object)): + '''Resampler/Replayer 的基类,用于处理时间边界、compression 和 bar 聚合。''' + params = ( ('bar2edge', True), ('adjbartime', True), @@ -116,14 +116,14 @@ def __init__(self, data): not (self.p.compression % data._compression)) self.bar = _Bar(maxdate=True) # bar holder - self.compcount = 0 # count of produced bars to control compression + self.compcount = 0 # 已产生 bar 的计数,用于控制 compression self._firstbar = True self.doadjusttime = (self.p.bar2edge and self.p.adjbartime and self.subweeks) self._nexteos = None - # Modify data information according to own parameters + # 按自身参数修改 data 信息 data.resampling = 1 data.replaying = self.replaying data._timeframe = self.p.timeframe @@ -132,11 +132,11 @@ def __init__(self, data): self.data = data def _latedata(self, data): - # new data at position 0, still untouched from stream + # new data 位于 position 0,尚未从 stream 中移除 if not self.subdays: return False - # Time already delivered + # 时间已交付 return len(data) > 1 and data.datetime[0] <= data.datetime[-1] def _checkbarover(self, data, fromcheck=False, forcedata=None): @@ -151,7 +151,7 @@ def _checkbarover(self, data, fromcheck=False, forcedata=None): elif not fromcheck: # fromcheck doesn't increase compcount self.compcount += 1 if not (self.compcount % self.p.compression): - # boundary crossed and enough bars for compression ... proceed + # 已跨过 boundary 且已有足够 bar 满足 compression,继续 isover = True return isover @@ -160,7 +160,7 @@ def _barover(self, data): tframe = self.p.timeframe if tframe == TimeFrame.Ticks: - # Ticks is already the lowest level + # Ticks 已是最低层级 return self.bar.isopen() elif tframe < TimeFrame.Days: @@ -193,10 +193,8 @@ def _eoscheck(self, data, seteos=True, exact=False): if exact: ret = equal else: - # if the compared data goes over the endofsession - # make sure the resampled bar is open and has something before that - # end of session. It could be a weekend and nothing was delivered - # until Monday + # 如果被比较 data 超过 endofsession,需要确认 resampled bar 已打开, + # 且在该 session end 之前已有内容。可能遇到周末,直到周一才有数据交付。 if grter: ret = (self.bar.isopen() and self.bar.datetime <= self._nextdteos) @@ -240,11 +238,10 @@ def _barover_years(self, data): data.num2date(self.bar.datetime).year) def _gettmpoint(self, tm): - '''Returns the point of time intraday for a given time according to the - timeframe + '''按 timeframe 返回给定 time 在日内对应的时间点。 - - Ex 1: 00:05:00 in minutes -> point = 5 - - Ex 2: 00:05:20 in seconds -> point = 5 * 60 + 20 = 320 + - 示例 1:00:05:00 按 minutes -> point = 5 + - 示例 2:00:05:20 按 seconds -> point = 5 * 60 + 20 = 320 ''' point = tm.hour * 60 + tm.minute restpoint = 0 @@ -270,7 +267,7 @@ def _barover_subdays(self, data): if data.datetime[0] < self.bar.datetime: return False - # Get time objects for the comparisons - in utc-like format + # 获取比较用 time object,采用 utc-like 格式 tm = num2date(self.bar.datetime).time() bartm = num2date(data.datetime[0]).time() @@ -279,30 +276,29 @@ def _barover_subdays(self, data): ret = False if barpoint > point: - # The data bar has surpassed the internal bar + # data bar 已超过内部 bar if not self.p.bar2edge: - # Compression done on simple bar basis (like days) + # 按简单 bar 计数完成 compression(类似 days) ret = True elif self.p.compression == 1: - # no bar compression requested -> internal bar done + # 未请求 bar compression,内部 bar 已完成 ret = True else: point_comp = point // self.p.compression barpoint_comp = barpoint // self.p.compression - # Went over boundary including compression + # 已跨过包含 compression 的 boundary if barpoint_comp > point_comp: ret = True return ret def check(self, data, _forcedata=None): - '''Called to check if the current stored bar has to be delivered in - spite of the data not having moved forward. If no ticks from a live - feed come in, a 5 second resampled bar could be delivered 20 seconds - later. When this method is called the wall clock (incl data time - offset) is called to check if the time has gone so far as to have to - deliver the already stored data + '''检查当前已保存 bar 是否应在 data 未推进时交付。 + + 如果 live feed 没有新 tick 进入,一个 5 秒 resampled bar 可能会在 20 秒后才交付。 + 调用该方法时,会使用 wall clock(包含 data time offset)检查时间是否已经推进到 + 必须交付已保存 data 的程度。 ''' if not self.bar.isopen(): return @@ -316,7 +312,7 @@ def _dataonedge(self, data): tframe = self.p.timeframe ret = False - if tframe == TimeFrame.Weeks: # Ticks is already the lowest + if tframe == TimeFrame.Weeks: # Ticks 已是最低层级 ret = data._calendar.last_weekday(data.datetime.date()) elif tframe == TimeFrame.Months: ret = data._calendar.last_monthday(data.datetime.date()) @@ -324,9 +320,8 @@ def _dataonedge(self, data): ret = data._calendar.last_yearday(data.datetime.date()) if ret: - # Data must be consumed but compression may not be met yet - # Prevent barcheckover from being called because it could again - # increase compcount + # Data 必须被消费,但 compression 可能尚未满足。 + # 防止调用 barcheckover,因为它可能再次增加 compcount。 docheckover = False self.compcount += 1 ret = not (self.compcount % self.p.compression) @@ -341,47 +336,47 @@ def _dataonedge(self, data): if self.subdays: point, prest = self._gettmpoint(data.datetime.time()) if prest: - return False, True # cannot be on boundary, subunits present + return False, True # 存在 subunits,不可能在 boundary 上 - # Pass through compression to get boundary and rest over boundary + # 通过 compression 获取 boundary 和超出 boundary 的余数 bound, brest = divmod(point, self.p.compression) - # if no extra and decomp bound is point + # 没有额外余数且 decomp 后 boundary 等于 point return (brest == 0 and point == (bound * self.p.compression), True) - # Code overriden by eoscheck + # 由 eoscheck 覆盖的代码 if False and self.p.sessionend: - # Days scenario - get datetime to compare in output timezone - # because p.sessionend is expected in output timezone + # Days 场景:在输出 timezone 中获取 datetime 进行比较, + # 因为 p.sessionend 预期位于输出 timezone。 bdtime = data.datetime.datetime() bsend = datetime.combine(bdtime.date(), data.p.sessionend) return bdtime == bsend - return False, True # subweeks, not subdays and not sessionend + return False, True # subweeks,但不是 subdays,也不是 sessionend def _calcadjtime(self, greater=False): if self._nexteos is None: - # Session has been exceeded - end of session is the mark + # Session 已超过,使用 end of session 作为标记 return self._lastdteos # utc-like dt = self.data.num2date(self.bar.datetime) - # Get current time + # 获取当前 time tm = dt.time() - # Get the point of the day in the time frame unit (ex: minute 200) + # 获取当天在 timeframe 单位上的 point(例如 minute 200) point, _ = self._gettmpoint(tm) - # Apply compression to update the point position (comp 5 -> 200 // 5) + # 应用 compression 更新 point 位置(comp 5 -> 200 // 5) # point = (point // self.p.compression) point = point // self.p.compression - # If rightedge (end of boundary is activated) add it unless recursing + # 如果启用 rightedge(boundary end),除递归场景外加上它 point += self.p.rightedge - # Restore point to the timeframe units by de-applying compression + # 反向应用 compression,将 point 恢复为 timeframe 单位 point *= self.p.compression - # Get hours, minutes, seconds and microseconds + # 获取 hours、minutes、seconds 和 microseconds extradays = 0 if self.p.timeframe == TimeFrame.Minutes: ph, pm = divmod(point, 60) @@ -396,18 +391,18 @@ def _calcadjtime(self, greater=False): pm, psec = divmod(pm, 60 * 1e6) ps, pus = divmod(psec, 1e6) elif self.p.timeframe == TimeFrame.Days: - # last resort + # 最后兜底 eost = self._nexteos.time() ph = eost.hour pm = eost.minute ps = eost.second pus = eost.microsecond - if ph > 23: # went over midnight: + if ph > 23: # 跨过午夜 extradays = ph // 24 ph %= 24 - # Replace intraday parts with the calculated ones and update it + # 用计算结果替换日内部分并更新 dt = dt.replace(hour=int(ph), minute=int(pm), second=int(ps), microsecond=int(pus)) if extradays: @@ -416,13 +411,10 @@ def _calcadjtime(self, greater=False): return dtnum def _adjusttime(self, greater=False, forcedata=None): - ''' - Adjusts the time of calculated bar (from underlying data source) by - using the timeframe to the appropriate boundary, with compression taken - into account + '''调整已计算 bar 的时间。 - Depending on param ``rightedge`` uses the starting boundary or the - ending one + 该方法根据 timeframe 和 compression,把底层 data source 得出的 bar 时间调整到 + 合适 boundary。根据参数 ``rightedge``,使用起始 boundary 或结束 boundary。 ''' dtnum = self._calcadjtime(greater=greater) if greater and dtnum <= self.bar.datetime: @@ -433,39 +425,37 @@ def _adjusttime(self, greater=False, forcedata=None): class Resampler(_BaseResampler): - '''This class resamples data of a given timeframe to a larger timeframe. + '''将给定 timeframe 的 data resample 到更大 timeframe。 - Params + Args: - - bar2edge (default: True) + - ``bar2edge`` (default: ``True``) - resamples using time boundaries as the target. For example with a - "ticks -> 5 seconds" the resulting 5 seconds bars will be aligned to - xx:00, xx:05, xx:10 ... + 使用时间 boundary 作为 resample 目标。例如 "ticks -> 5 seconds" 时, + 生成的 5 秒 bar 会对齐到 xx:00、xx:05、xx:10 ... - - adjbartime (default: True) + - ``adjbartime`` (default: ``True``) - Use the time at the boundary to adjust the time of the delivered - resampled bar instead of the last seen timestamp. If resampling to "5 - seconds" the time of the bar will be adjusted for example to hh:mm:05 - even if the last seen timestamp was hh:mm:04.33 + 使用 boundary 时间调整交付的 resampled bar 时间,而不是使用最后看到的 + timestamp。例如 resample 到 "5 seconds" 时,即使最后看到的 timestamp 是 + hh:mm:04.33,bar 时间也会被调整到 hh:mm:05。 .. note:: - Time will only be adjusted if "bar2edge" is True. It wouldn't make - sense to adjust the time if the bar has not been aligned to a - boundary + 只有 "bar2edge" 为 True 时才会调整时间。如果 bar 未对齐到 boundary, + 调整时间没有意义。 + + - ``rightedge`` (default: ``True``) - - rightedge (default: True) + 使用时间 boundary 的右边界来设置时间。 - Use the right edge of the time boundaries to set the time. + 如果为 ``False`` 且压缩到 5 秒,则 hh:mm:00 到 hh:mm:04 之间生成的 + resampled bar 时间会是 hh:mm:00(起始 boundary)。 - If False and compressing to 5 seconds the time of a resampled bar for - seconds between hh:mm:00 and hh:mm:04 will be hh:mm:00 (the starting - boundary + 如果为 ``True``,用于时间的 boundary 会是 hh:mm:05(结束 boundary)。 - If True the used boundary for the time will be hh:mm:05 (the ending - boundary) + Returns: + Resampler: 将输入 data 聚合为更大 timeframe bar 的 filter。 ''' params = ( ('bar2edge', True), @@ -476,24 +466,22 @@ class Resampler(_BaseResampler): replaying = False def last(self, data): - '''Called when the data is no longer producing bars + '''当 data 不再产生 bar 时调用。 - Can be called multiple times. It has the chance to (for example) - produce extra bars which may still be accumulated and have to be - delivered + 该方法可能被多次调用。它可以用于生成仍在累计、尚需交付的额外 bar。 ''' if self.bar.isopen(): if self.doadjusttime: self._adjusttime() data._add2stack(self.bar.lvalues()) - self.bar.bstart(maxdate=True) # close the bar to avoid dups + self.bar.bstart(maxdate=True) # 关闭 bar,避免重复 return True return False def __call__(self, data, fromcheck=False, forcedata=None): - '''Called for each set of values produced by the data source''' + '''对 data source 产生的每组 value 调用。''' consumed = False onedge = False docheckover = True @@ -501,40 +489,40 @@ def __call__(self, data, fromcheck=False, forcedata=None): if self._latedata(data): if not self.p.takelate: data.backwards() - return True # get a new bar + return True # 获取 new bar - self.bar.bupdate(data) # update new or existing bar - # push time beyond reference + self.bar.bupdate(data) # 更新 new 或 existing bar + # 将时间推到 reference 之后 self.bar.datetime = data.datetime[-1] + 0.000001 - data.backwards() # remove used bar + data.backwards() # 移除已用 bar return True - if self.componly: # only if not subdays - # Get a session ref before rewinding + if self.componly: # 仅在非 subdays 时 + # rewinding 前获取 session ref _, self._lastdteos = self.data._getnexteos() consumed = True else: - onedge, docheckover = self._dataonedge(data) # for subdays + onedge, docheckover = self._dataonedge(data) # 用于 subdays consumed = onedge if consumed: - self.bar.bupdate(data) # update new or existing bar - data.backwards() # remove used bar + self.bar.bupdate(data) # 更新 new 或 existing bar + data.backwards() # 移除已用 bar # if self.bar.isopen and (onedge or (docheckover and checkbarover)) cond = self.bar.isopen() - if cond: # original is and, the 2nd term must also be true - if not onedge: # onedge true is sufficient + if cond: # 原始逻辑是 and,第二项也必须为 true + if not onedge: # onedge 为 true 已足够 if docheckover: cond = self._checkbarover(data, fromcheck=fromcheck, forcedata=forcedata) if cond: dodeliver = False if forcedata is not None: - # check our delivery time is not larger than that of forcedata + # 检查交付时间不能大于 forcedata 的时间 tframe = self.p.timeframe - if tframe == TimeFrame.Ticks: # Ticks is already the lowest + if tframe == TimeFrame.Ticks: # Ticks 已是最低层级 dodeliver = True elif tframe == TimeFrame.Minutes: dtnum = self._calcadjtime(greater=True) @@ -550,59 +538,56 @@ def __call__(self, data, fromcheck=False, forcedata=None): self._adjusttime(greater=True, forcedata=forcedata) data._add2stack(self.bar.lvalues()) - self.bar.bstart(maxdate=True) # bar delivered -> restart + self.bar.bstart(maxdate=True) # bar 已交付 -> restart if not fromcheck: if not consumed: - self.bar.bupdate(data) # update new or existing bar - data.backwards() # remove used bar + self.bar.bupdate(data) # 更新 new 或 existing bar + data.backwards() # 移除已用 bar return True class Replayer(_BaseResampler): - '''This class replays data of a given timeframe to a larger timeframe. + '''将给定 timeframe 的 data replay 到更大 timeframe。 - It simulates the action of the market by slowly building up (for ex.) a - daily bar from tick/seconds/minutes data + 它会用 tick/seconds/minutes data 逐步构建更大 bar(例如 daily bar),以模拟 + 市场实时形成 bar 的过程。 - Only when the bar is complete will the "length" of the data be changed - effectively delivering a closed bar + 只有 bar 完成时,data 的 "length" 才会真正变化,从而交付一个 closed bar。 - Params + Args: - - bar2edge (default: True) + - ``bar2edge`` (default: ``True``) - replays using time boundaries as the target of the closed bar. For - example with a "ticks -> 5 seconds" the resulting 5 seconds bars will - be aligned to xx:00, xx:05, xx:10 ... + 使用时间 boundary 作为 closed bar 的目标。例如 "ticks -> 5 seconds" 时, + 生成的 5 秒 bar 会对齐到 xx:00、xx:05、xx:10 ... - - adjbartime (default: False) + - ``adjbartime`` (default: ``False``) - Use the time at the boundary to adjust the time of the delivered - resampled bar instead of the last seen timestamp. If resampling to "5 - seconds" the time of the bar will be adjusted for example to hh:mm:05 - even if the last seen timestamp was hh:mm:04.33 + 使用 boundary 时间调整交付的 resampled bar 时间,而不是使用最后看到的 + timestamp。例如 resample 到 "5 seconds" 时,即使最后看到的 timestamp 是 + hh:mm:04.33,bar 时间也会被调整到 hh:mm:05。 .. note:: - Time will only be adjusted if "bar2edge" is True. It wouldn't make - sense to adjust the time if the bar has not been aligned to a - boundary + 只有 "bar2edge" 为 True 时才会调整时间。如果 bar 未对齐到 boundary, + 调整时间没有意义。 + + .. note:: 如果该参数为 True,会在 *replayed* bar 末尾引入一个带 *adjusted* + time 的额外 tick。 - .. note:: if this parameter is True an extra tick with the *adjusted* - time will be introduced at the end of the *replayed* bar + - ``rightedge`` (default: ``True``) - - rightedge (default: True) + 使用时间 boundary 的右边界来设置时间。 - Use the right edge of the time boundaries to set the time. + 如果为 ``False`` 且压缩到 5 秒,则 hh:mm:00 到 hh:mm:04 之间生成的 + resampled bar 时间会是 hh:mm:00(起始 boundary)。 - If False and compressing to 5 seconds the time of a resampled bar for - seconds between hh:mm:00 and hh:mm:04 will be hh:mm:00 (the starting - boundary + 如果为 ``True``,用于时间的 boundary 会是 hh:mm:05(结束 boundary)。 - If True the used boundary for the time will be hh:mm:05 (the ending - boundary) + Returns: + Replayer: 逐步构造更大 timeframe bar 的 replay filter。 ''' params = ( ('bar2edge', True), @@ -622,19 +607,19 @@ def __call__(self, data, fromcheck=False, forcedata=None): if self._latedata(data): if not self.p.takelate: data.backwards(force=True) - return True # get a new bar + return True # 获取 new bar consumed = True takinglate = True - elif self.componly: # only if not subdays + elif self.componly: # 仅在非 subdays 时 consumed = True else: - onedge, docheckover = self._dataonedge(data) # for subdays + onedge, docheckover = self._dataonedge(data) # 用于 subdays consumed = onedge - data._tick_fill(force=True) # update + data._tick_fill(force=True) # 更新 if consumed: self.bar.bupdate(data) @@ -643,61 +628,60 @@ def __call__(self, data, fromcheck=False, forcedata=None): # if onedge or (checkbarover and self._checkbarover) cond = onedge - if not cond: # original is or, if true it would suffice + if not cond: # 原始逻辑是 or,若为 true 即可满足 if docheckover: cond = self._checkbarover(data, fromcheck=fromcheck) if cond: - if not onedge and self.doadjusttime: # insert tick with adjtime + if not onedge and self.doadjusttime: # 插入带 adjtime 的 tick adjusted = self._adjusttime(greater=True) if adjusted: ago = 0 if (consumed or fromcheck) else -1 - # Update to the point right before the new data + # 更新到 new data 之前的那个点 data._updatebar(self.bar.lvalues(), forward=False, ago=ago) if not fromcheck: if not consumed: - # Reopen bar with real new data and save data to queue + # 用真实 new data 重新打开 bar,并把 data 保存到 queue self.bar.bupdate(data, reopen=True) - # erase is True, but the tick will not be seen below - # and therefore no need to mark as 1st + # erase 为 True,但 tick 不会在下面被看到,因此无需标记为第 1 个 data._save2stack(erase=True, force=True) else: self.bar.bstart(maxdate=True) - self._firstbar = True # next is first + self._firstbar = True # next 是 first else: # from check - # fromcheck or consumed have forced delivery, reopen + # fromcheck 或 consumed 已强制交付,重新打开 self.bar.bstart(maxdate=True) - self._firstbar = True # next is first + self._firstbar = True # next 是 first if adjusted: - # after adjusting need to redeliver if this was a check + # 如果这是 check,调整后需要重新交付 data._save2stack(erase=True, force=True) elif not fromcheck: if not consumed: - # Data already "forwarded" and we replay to new bar - # No need to go backwards. simply reopen internal cache + # Data 已经 "forwarded",并且 replay 到 new bar。 + # 无需 backwards,直接重新打开内部 cache。 self.bar.bupdate(data, reopen=True) else: - # compression only, used data to update bar, hence remove - # from stream, update existing data, reopen bar - if not self._firstbar: # only discard data if not firstbar + # 仅 compression:已用 data 更新 bar,因此从 stream 中移除, + # 更新 existing data,并重新打开 bar。 + if not self._firstbar: # 仅在不是 firstbar 时丢弃 data data.backwards(force=True) data._updatebar(self.bar.lvalues(), forward=False, ago=0) self.bar.bstart(maxdate=True) - self._firstbar = True # make sure next tick moves forward + self._firstbar = True # 确保 next tick 向前推进 elif not fromcheck: - # not over, update, remove new entry, deliver + # 尚未结束:更新、移除 new entry、交付 if not consumed: self.bar.bupdate(data) - if not self._firstbar: # only discard data if not firstbar + if not self._firstbar: # 仅在不是 firstbar 时丢弃 data data.backwards(force=True) data._updatebar(self.bar.lvalues(), forward=False, ago=0) self._firstbar = False - return False # the existing bar can be processed by the system + return False # existing bar 可由系统处理 class ResamplerTicks(Resampler): diff --git a/backtrader/signal.py b/backtrader/signal.py index 74e41b2bb..cf2259946 100644 --- a/backtrader/signal.py +++ b/backtrader/signal.py @@ -54,6 +54,8 @@ class Signal(bt.Indicator): + '''signal indicator 的包装类,用于把输入 line 暴露为 ``signal`` line。''' + SignalTypes = SignalTypes lines = ('signal',) diff --git a/backtrader/sizer.py b/backtrader/sizer.py index c78f837bb..a1d7b2223 100644 --- a/backtrader/sizer.py +++ b/backtrader/sizer.py @@ -27,56 +27,81 @@ class Sizer(with_metaclass(MetaParams, object)): - '''This is the base class for *Sizers*. Any *sizer* should subclass this - and override the ``_getsizing`` method + '''*Sizers* 的基类。任何 *sizer* 都应继承该类并覆盖 ``_getsizing`` 方法。 - Member Attribs: + 成员属性: - - ``strategy``: will be set by the strategy in which the sizer is working + - ``strategy``: 由使用该 sizer 的 strategy 设置 - Gives access to the entire api of the strategy, for example if the - actual data position would be needed in ``_getsizing``:: + 可访问 strategy 的完整 api。例如,如果 ``_getsizing`` 需要实际 data + position,可以使用:: position = self.strategy.getposition(data) - - ``broker``: will be set by the strategy in which the sizer is working - - Gives access to information some complex sizers may need like portfolio - value, .. + - ``broker``: 由使用该 sizer 的 strategy 设置 + + 可访问复杂 sizer 可能需要的信息,例如 portfolio value 等。 + + --- + 交互示例: + + >>> class FixedSizer(Sizer): + ... def _getsizing(self, comminfo, cash, data, isbuy): + ... return 10 + >>> class Broker: + ... def getcommissioninfo(self, data): + ... return None + ... def getcash(self): + ... return 1000.0 + >>> sizer = FixedSizer() + >>> sizer.set(strategy='strategy', broker=Broker()) + >>> sizer.getsizing(data='data0', isbuy=True) + 10 ''' strategy = None broker = None def getsizing(self, data, isbuy): + '''返回给定 data 和买卖方向下的实际 size。 + + Args: + data: 操作目标 data。 + isbuy (bool): ``True`` 表示 buy 操作,``False`` 表示 sell 操作。 + + Returns: + int: 由 ``_getsizing`` 计算得到的实际 size。 + ''' comminfo = self.broker.getcommissioninfo(data) return self._getsizing(comminfo, self.broker.getcash(), data, isbuy) def _getsizing(self, comminfo, cash, data, isbuy): - '''This method has to be overriden by subclasses of Sizer to provide - the sizing functionality + '''子类必须覆盖该方法,以提供 sizing 功能。 - Params: - - ``comminfo``: The CommissionInfo instance that contains - information about the commission for the data and allows - calculation of position value, operation cost, commision for the - operation + Args: + comminfo: CommissionInfo 实例,包含该 data 的 commission 信息,并可 + 用于计算 position value、operation cost 和 operation commission。 - - ``cash``: current available cash in the *broker* + cash (float): *broker* 当前可用 cash。 - - ``data``: target of the operation + data: 操作目标。 - - ``isbuy``: will be ``True`` for *buy* operations and ``False`` - for *sell* operations + isbuy (bool): *buy* 操作为 ``True``,*sell* 操作为 ``False``。 - The method has to return the actual size (an int) to be executed. If - ``0`` is returned nothing will be executed. + Returns: + int: 要执行的实际 size。如果返回 ``0``,则不会执行任何操作。 - The absolute value of the returned value will be used + 返回值会使用其绝对值。 ''' raise NotImplementedError def set(self, strategy, broker): + '''设置 sizer 所属的 strategy 和 broker。 + + Args: + strategy: 使用该 sizer 的 strategy。 + broker: strategy 对应的 broker。 + ''' self.strategy = strategy self.broker = broker diff --git a/backtrader/sizers/__init__.py b/backtrader/sizers/__init__.py index b79ab6cb0..82839d885 100644 --- a/backtrader/sizers/__init__.py +++ b/backtrader/sizers/__init__.py @@ -21,8 +21,8 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) -# The modules below should/must define __all__ with the objects wishes -# or prepend an "_" (underscore) to private classes/variables +# 下方模块应定义 __all__ 来声明导出对象,或给私有 class/variable 加上 +# "_" 前缀 from .fixedsize import * from .percents_sizer import * diff --git a/backtrader/sizers/fixedsize.py b/backtrader/sizers/fixedsize.py index adf7be18c..bc7ee34b0 100644 --- a/backtrader/sizers/fixedsize.py +++ b/backtrader/sizers/fixedsize.py @@ -25,16 +25,25 @@ class FixedSize(bt.Sizer): - ''' - This sizer simply returns a fixed size for any operation. - Size can be controlled by number of tranches that a system - wishes to use to scale into trades by specifying the ``tranches`` - parameter. + '''返回固定 size 的 sizer。 + + 可以通过 ``tranches`` 参数把 ``stake`` 拆成多份,用于分批建仓。 + + Args: + stake (int): 每次操作使用的基础 size,默认 ``1``。 + tranches (int): 分批数量,默认 ``1``。大于 ``1`` 时返回 + ``stake / tranches`` 的整数部分。 + Returns: + int: ``_getsizing`` 返回固定 size 或分批后的 size。 - Params: - - ``stake`` (default: ``1``) - - ``tranches`` (default: ``1``) + --- + >>> sizer = FixedSize(stake=10) + >>> sizer._getsizing(None, 1000.0, None, True) + 10 + >>> sizer = FixedSize(stake=10, tranches=2) + >>> sizer._getsizing(None, 1000.0, None, True) + 5 ''' params = (('stake', 1), @@ -50,22 +59,34 @@ def setsizing(self, stake): if self.p.tranches > 1: self.p.stake = abs(int(self.p.stake / self.p.tranches)) else: - self.p.stake = stake # OLD METHOD FOR SAMPLE COMPATIBILITY + self.p.stake = stake # 旧方法,为 sample 兼容保留 SizerFix = FixedSize class FixedReverser(bt.Sizer): - '''This sizer returns the needes fixed size to reverse an open position or - the fixed size to open one - - - To open a position: return the param ``stake`` - - - To reverse a position: return 2 * ``stake`` - - Params: - - ``stake`` (default: ``1``) + '''返回固定 size,并在反手时返回双倍 size 的 sizer。 + + 无持仓时返回 ``stake``,已有持仓时返回 ``2 * stake``,便于一次操作完成 + 平旧仓并开新仓。 + + Args: + stake (int): 开仓使用的基础 size,默认 ``1``。 + + Returns: + int: 当前无持仓时为 ``stake``;已有持仓时为 ``2 * stake``。 + + --- + >>> class Position: + ... size = 3 + >>> class Strategy: + ... def getposition(self, data): + ... return Position() + >>> sizer = FixedReverser(stake=5) + >>> sizer.strategy = Strategy() + >>> sizer._getsizing(None, 1000.0, None, False) + 10 ''' params = (('stake', 1),) @@ -76,17 +97,29 @@ def _getsizing(self, comminfo, cash, data, isbuy): class FixedSizeTarget(bt.Sizer): - ''' - This sizer simply returns a fixed target size, useful when coupled - with Target Orders and specifically ``cerebro.target_order_size()``. - Size can be controlled by number of tranches that a system - wishes to use to scale into trades by specifying the ``tranches`` - parameter. - - - Params: - - ``stake`` (default: ``1``) - - ``tranches`` (default: ``1``) + '''返回固定 target size 的 sizer。 + + 该 sizer 适合配合 Target Orders 使用,尤其是 + ``cerebro.target_order_size()``。也可以通过 ``tranches`` 参数分批靠近 + 目标 size。 + + Args: + stake (int): 目标 size,默认 ``1``。 + tranches (int): 分批数量,默认 ``1``。大于 ``1`` 时,每次最多增加 + ``stake / tranches`` 的整数部分。 + + Returns: + int: 目标 size,或在分批模式下不超过 ``stake`` 的下一步 target size。 + + --- + >>> class Position: + ... size = 4 + >>> class Strategy: + ... position = Position() + >>> sizer = FixedSizeTarget(stake=10, tranches=2) + >>> sizer.strategy = Strategy() + >>> sizer._getsizing(None, 1000.0, None, True) + 9 ''' params = (('stake', 1), @@ -105,4 +138,4 @@ def setsizing(self, stake): self.p.stake = min((self.strategy.position.size + size), self.p.stake) else: - self.p.stake = stake # OLD METHOD FOR SAMPLE COMPATIBILITY + self.p.stake = stake # 旧方法,为 sample 兼容保留 diff --git a/backtrader/sizers/percents_sizer.py b/backtrader/sizers/percents_sizer.py index d9e3eeeb0..cb523689f 100644 --- a/backtrader/sizers/percents_sizer.py +++ b/backtrader/sizers/percents_sizer.py @@ -27,15 +27,36 @@ class PercentSizer(bt.Sizer): - '''This sizer return percents of available cash - - Params: - - ``percents`` (default: ``20``) + '''按可用 cash 的百分比返回 size 的 sizer。 + + 如果当前 data 没有持仓,则用 ``cash / data.close[0] * percents / 100`` + 计算 size;如果已经有持仓,则返回当前 position size。 + + Args: + percents (float): 使用可用 cash 的百分比,默认 ``20``。 + retint (bool): 是否把返回 size 截断为 int,默认 ``False``。 + + Returns: + float | int: 计算得到的 size;``retint`` 为 ``True`` 时返回 int。 + + --- + >>> class Broker: + ... def getposition(self, data): + ... return 0 + >>> class Close: + ... def __getitem__(self, ago): + ... return 10.0 + >>> class Data: + ... close = Close() + >>> sizer = PercentSizer(percents=25) + >>> sizer.broker = Broker() + >>> sizer._getsizing(None, 1000.0, Data(), True) + 25.0 ''' params = ( ('percents', 20), - ('retint', False), # return an int size or rather the float value + ('retint', False), # 返回 int size,还是保留 float value ) def __init__(self): @@ -55,10 +76,18 @@ def _getsizing(self, comminfo, cash, data, isbuy): class AllInSizer(PercentSizer): - '''This sizer return all available cash of broker + '''使用 broker 全部可用 cash 计算 size 的 sizer。 + + Args: + percents (float): 使用可用 cash 的百分比,默认 ``100``。 - Params: - - ``percents`` (default: ``100``) + Returns: + float: 计算得到的全仓 size。 + + --- + >>> sizer = AllInSizer() + >>> sizer.p.percents + 100 ''' params = ( ('percents', 100), @@ -66,24 +95,38 @@ class AllInSizer(PercentSizer): class PercentSizerInt(PercentSizer): - '''This sizer return percents of available cash in form of size truncated - to an int + '''按可用 cash 的百分比返回 size,并将结果截断为 int 的 sizer。 + + Args: + percents (float): 使用可用 cash 的百分比,默认 ``20``。 - Params: - - ``percents`` (default: ``20``) + Returns: + int: 截断为 int 的 size。 + + --- + >>> sizer = PercentSizerInt() + >>> sizer.p.retint + True ''' params = ( - ('retint', True), # return an int size or rather the float value + ('retint', True), # 返回 int size,还是保留 float value ) class AllInSizerInt(PercentSizerInt): - '''This sizer return all available cash of broker with the - size truncated to an int + '''使用 broker 全部可用 cash 计算 size,并将结果截断为 int 的 sizer。 + + Args: + percents (float): 使用可用 cash 的百分比,默认 ``100``。 + + Returns: + int: 截断为 int 的全仓 size。 - Params: - - ``percents`` (default: ``100``) + --- + >>> sizer = AllInSizerInt() + >>> sizer.p.percents, sizer.p.retint + (100, True) ''' params = ( ('percents', 100), diff --git a/backtrader/store.py b/backtrader/store.py index a1352671d..13612882b 100644 --- a/backtrader/store.py +++ b/backtrader/store.py @@ -28,7 +28,8 @@ class MetaSingleton(MetaParams): - '''Metaclass to make a metaclassed class a singleton''' + '''singleton metaclass 的基类,用于让使用该 metaclass 的类保持单例。''' + def __init__(cls, name, bases, dct): super(MetaSingleton, cls).__init__(name, bases, dct) cls._singleton = None @@ -42,29 +43,46 @@ def __call__(cls, *args, **kwargs): class Store(with_metaclass(MetaSingleton, object)): - '''Base class for all Stores''' + '''Store 的基类,用于统一管理 broker/data 的注册、启动和通知。''' _started = False params = () def getdata(self, *args, **kwargs): - '''Returns ``DataCls`` with args, kwargs''' + '''创建并返回注册的 ``DataCls`` 实例。 + + Args: + *args: 传给 ``DataCls`` 的位置参数。 + **kwargs: 传给 ``DataCls`` 的关键字参数。 + + Returns: + DataCls: 已绑定当前 store 的 data 实例。 + ''' data = self.DataCls(*args, **kwargs) data._store = self return data @classmethod def getbroker(cls, *args, **kwargs): - '''Returns broker with *args, **kwargs from registered ``BrokerCls``''' + '''创建并返回注册的 ``BrokerCls`` 实例。 + + Args: + *args: 传给 ``BrokerCls`` 的位置参数。 + **kwargs: 传给 ``BrokerCls`` 的关键字参数。 + + Returns: + BrokerCls: 已绑定当前 store 类的 broker 实例。 + ''' broker = cls.BrokerCls(*args, **kwargs) broker._store = cls return broker - BrokerCls = None # broker class will autoregister - DataCls = None # data class will auto register + BrokerCls = None # broker class 会自动注册 + DataCls = None # data class 会自动注册 def start(self, data=None, broker=None): + '''启动 store,并按需关联 data 或 broker。''' if not self._started: self._started = True self.notifs = collections.deque() @@ -83,12 +101,14 @@ def start(self, data=None, broker=None): self.broker = broker def stop(self): + '''停止 store 的 hook。''' pass def put_notification(self, msg, *args, **kwargs): + '''保存一条 store notification。''' self.notifs.append((msg, args, kwargs)) def get_notifications(self): - '''Return the pending "store" notifications''' - self.notifs.append(None) # put a mark / threads could still append + '''返回待处理的 store notification。''' + self.notifs.append(None) # 放置标记;其它线程仍可能继续 append return [x for x in iter(self.notifs.popleft, None)] diff --git a/backtrader/stores/__init__.py b/backtrader/stores/__init__.py index f60afb6ad..a450cb3f0 100644 --- a/backtrader/stores/__init__.py +++ b/backtrader/stores/__init__.py @@ -21,23 +21,22 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) -# The modules below should/must define __all__ with the objects wishes -# or prepend an "_" (underscore) to private classes/variables +# 下列模块应在 __all__ 中定义要导出的对象,私有类/变量则使用 "_" 前缀 try: from .ibstore import IBStore except ImportError: - pass # The user may not have ibpy installed + pass # 用户可能未安装 ibpy try: from .vcstore import VCStore except ImportError: - pass # The user may not have a module installed + pass # 用户可能未安装相关模块 try: from .oandastore import OandaStore except ImportError: - pass # The user may not have a module installed + pass # 用户可能未安装相关模块 from .vchartfile import VChartFile diff --git a/backtrader/stores/ibstore.py b/backtrader/stores/ibstore.py index 00ba5e11b..0166917a0 100644 --- a/backtrader/stores/ibstore.py +++ b/backtrader/stores/ibstore.py @@ -38,11 +38,11 @@ from backtrader.utils.py3 import bytes, bstr, queue, with_metaclass, long from backtrader.utils import AutoDict, UTC -bytes = bstr # py2/3 need for ibpy +bytes = bstr # ibpy 需要 py2/3 兼容 bytes def _ts2dt(tstamp=None): - # Transforms a RTVolume timestamp to a datetime object + # 将 RTVolume timestamp 转为 datetime 对象 if not tstamp: return datetime.utcnow() @@ -52,10 +52,9 @@ def _ts2dt(tstamp=None): class RTVolume(object): - '''Parses a tickString tickType 48 (RTVolume) event from the IB API into its - constituent fields + '''将 IB API 中 tickString tickType 48(RTVolume)事件解析为组成字段。 - Supports using a "price" to simulate an RTVolume from a tickPrice event + 支持使用 "price" 从 tickPrice event 模拟 RTVolume。 ''' _fields = [ ('price', float), @@ -67,14 +66,14 @@ class RTVolume(object): ] def __init__(self, rtvol='', price=None, tmoffset=None): - # Use a provided string or simulate a list of empty tokens + # 使用传入字符串,或模拟一个空 token 列表 tokens = iter(rtvol.split(';')) - # Put the tokens as attributes using the corresponding func + # 使用对应 func 将 token 放入属性 for name, func in self._fields: setattr(self, name, func(next(tokens)) if rtvol else func()) - # If price was provided use it + # 如提供 price,则使用该值 if price is not None: self.price = price @@ -83,7 +82,7 @@ def __init__(self, rtvol='', price=None, tmoffset=None): class MetaSingleton(MetaParams): - '''Metaclass to make a metaclassed class a singleton''' + '''让带 metaclass 的类成为 singleton 的 metaclass。''' def __init__(cls, name, bases, dct): super(MetaSingleton, cls).__init__(name, bases, dct) cls._singleton = None @@ -96,82 +95,39 @@ def __call__(cls, *args, **kwargs): return cls._singleton -# Decorator to mark methods to register with ib.opt +# 标记方法需要注册到 ib.opt 的 decorator def ibregister(f): f._ibregister = True return f class IBStore(with_metaclass(MetaSingleton, object)): - '''Singleton class wrapping an ibpy ibConnection instance. - - The parameters can also be specified in the classes which use this store, - like ``IBData`` and ``IBBroker`` - - Params: - - - ``host`` (default:``127.0.0.1``): where IB TWS or IB Gateway are - actually running. And although this will usually be the localhost, it - must not be - - - ``port`` (default: ``7496``): port to connect to. The demo system uses - ``7497`` - - - ``clientId`` (default: ``None``): which clientId to use to connect to - TWS. - - ``None``: generates a random id between 1 and 65535 - An ``integer``: will be passed as the value to use. - - - ``notifyall`` (default: ``False``) - - If ``False`` only ``error`` messages will be sent to the - ``notify_store`` methods of ``Cerebro`` and ``Strategy``. - - If ``True``, each and every message received from TWS will be notified - - - ``_debug`` (default: ``False``) - - Print all messages received from TWS to standard output - - - ``reconnect`` (default: ``3``) - - Number of attempts to try to reconnect after the 1st connection attempt - fails - - Set it to a ``-1`` value to keep on reconnecting forever - - - ``timeout`` (default: ``3.0``) - - Time in seconds between reconnection attemps - - - ``timeoffset`` (default: ``True``) - - If True, the time obtained from ``reqCurrentTime`` (IB Server time) - will be used to calculate the offset to localtime and this offset will - be used for the price notifications (tickPrice events, for example for - CASH markets) to modify the locally calculated timestamp. - - The time offset will propagate to other parts of the ``backtrader`` - ecosystem like the **resampling** to align resampling timestamps using - the calculated offset. - - - ``timerefresh`` (default: ``60.0``) - - Time in seconds: how often the time offset has to be refreshed - - - ``indcash`` (default: ``True``) - - Manage IND codes as if they were cash for price retrieval + '''封装 ibpy ibConnection 实例的 singleton store。 + + 参数也可在使用该 store 的类中指定,例如 ``IBData`` 和 ``IBBroker``。 + + Args: + host: IB TWS 或 IB Gateway 实际运行的 host,通常是 localhost,但并非必须。 + port: 连接端口。demo 系统使用 ``7497``。 + clientId: 连接 TWS 使用的 clientId。``None`` 表示随机生成 1 到 65535 之间的 id。 + notifyall: 是否将收到的所有 TWS 消息都通知给 ``notify_store``。 + _debug: 是否将收到的全部 TWS 消息打印到标准输出。 + reconnect: 第一次连接失败后的重连次数;``-1`` 表示永久重连。 + timeout: 重连尝试之间的秒数。 + timeoffset: 是否用 ``reqCurrentTime`` 获取 IB Server time 并计算本地时间偏移。 + timerefresh: 刷新 time offset 的秒数间隔。 + indcash: 是否像 cash 一样管理 IND 代码以获取价格。 + + Returns: + IBStore: 用于管理 IB 连接、data request、order/account 回调的 store。 ''' - # Set a base for the data requests (historical/realtime) to distinguish the - # id in the error notifications from orders, where the basis (usually - # starting at 1) is set by TWS + # 为 data request(historical/realtime)设置 id 基准,以便在 error notification 中 + # 与 order id 区分。order id 的基准通常由 TWS 设置,并从 1 附近开始。 REQIDBASE = 0x01000000 - BrokerCls = None # broker class will autoregister - DataCls = None # data class will auto register + BrokerCls = None # broker class 会自动注册 + DataCls = None # data class 会自动注册 params = ( ('host', '127.0.0.1'), @@ -188,78 +144,78 @@ class IBStore(with_metaclass(MetaSingleton, object)): @classmethod def getdata(cls, *args, **kwargs): - '''Returns ``DataCls`` with args, kwargs''' + '''使用 args/kwargs 返回 ``DataCls`` 实例。''' return cls.DataCls(*args, **kwargs) @classmethod def getbroker(cls, *args, **kwargs): - '''Returns broker with *args, **kwargs from registered ``BrokerCls``''' + '''使用注册的 ``BrokerCls`` 与 args/kwargs 返回 broker。''' return cls.BrokerCls(*args, **kwargs) def __init__(self): super(IBStore, self).__init__() - self._lock_q = threading.Lock() # sync access to _tickerId/Queues - self._lock_accupd = threading.Lock() # sync account updates - self._lock_pos = threading.Lock() # sync account updates - self._lock_notif = threading.Lock() # sync access to notif queue + self._lock_q = threading.Lock() # 同步访问 _tickerId/Queues + self._lock_accupd = threading.Lock() # 同步 account 更新 + self._lock_pos = threading.Lock() # 同步 position 更新 + self._lock_notif = threading.Lock() # 同步访问 notification queue - # Account list received + # account list 已接收 self._event_managed_accounts = threading.Event() self._event_accdownload = threading.Event() - self.dontreconnect = False # for non-recoverable connect errors + self.dontreconnect = False # 用于不可恢复连接错误 - self._env = None # reference to cerebro for general notifications - self.broker = None # broker instance - self.datas = list() # datas that have registered over start - self.ccount = 0 # requests to start (from cerebro or datas) + self._env = None # 指向 cerebro,用于通用通知 + self.broker = None # broker 实例 + self.datas = list() # start 期间注册的 data + self.ccount = 0 # 来自 cerebro 或 data 的 start request 数量 self._lock_tmoffset = threading.Lock() - self.tmoffset = timedelta() # to control time difference with server + self.tmoffset = timedelta() # 控制与 server 的时间差 - # Structures to hold datas requests + # 保存 data request 的结构 self.qs = collections.OrderedDict() # key: tickerId -> queues self.ts = collections.OrderedDict() # key: queue -> tickerId - self.iscash = dict() # tickerIds from cash products (for ex: EUR.JPY) + self.iscash = dict() # cash 产品的 tickerId(例如 EUR.JPY) - self.histexreq = dict() # holds segmented historical requests - self.histfmt = dict() # holds datetimeformat for request - self.histsend = dict() # holds sessionend (data time) for request - self.histtz = dict() # holds sessionend (data time) for request + self.histexreq = dict() # 保存分段 historical request + self.histfmt = dict() # 保存 request 的 datetimeformat + self.histsend = dict() # 保存 request 的 sessionend(data time) + self.histtz = dict() # 保存 request 的 timezone - self.acc_cash = AutoDict() # current total cash per account - self.acc_value = AutoDict() # current total value per account - self.acc_upds = AutoDict() # current account valueinfos per account + self.acc_cash = AutoDict() # 每个 account 当前 total cash + self.acc_value = AutoDict() # 每个 account 当前 total value + self.acc_upds = AutoDict() # 每个 account 当前 value info - self.port_update = False # indicate whether to signal to broker + self.port_update = False # 指示是否需要通知 broker - self.positions = collections.defaultdict(Position) # actual positions + self.positions = collections.defaultdict(Position) # 实际 position - self._tickerId = itertools.count(self.REQIDBASE) # unique tickerIds - self.orderid = None # next possible orderid (will be itertools.count) + self._tickerId = itertools.count(self.REQIDBASE) # 唯一 tickerId + self.orderid = None # 下一个可用 orderid(会是 itertools.count) - self.cdetails = collections.defaultdict(list) # hold cdetails requests + self.cdetails = collections.defaultdict(list) # 保存 cdetails request - self.managed_accounts = list() # received via managedAccounts + self.managed_accounts = list() # 通过 managedAccounts 接收 - self.notifs = queue.Queue() # store notifications for cerebro + self.notifs = queue.Queue() # 发送给 cerebro 的 store 通知 - # Use the provided clientId or a random one + # 使用提供的 clientId,或随机生成一个 if self.p.clientId is None: self.clientId = random.randint(1, pow(2, 16) - 1) else: self.clientId = self.p.clientId - # ibpy connection object + # ibpy connection 对象 self.conn = ibopt.ibConnection( host=self.p.host, port=self.p.port, clientId=self.clientId) - # register a printall method if requested + # 按需注册 printall 方法 if self.p._debug or self.p.notifyall: self.conn.registerAll(self.watcher) - # Register decorated methods with the conn + # 将带 decorator 的方法注册到 conn methods = inspect.getmembers(self, inspect.ismethod) for name, method in methods: if not getattr(method, '_ibregister', False): @@ -268,46 +224,44 @@ def __init__(self): message = getattr(ibopt.message, name) self.conn.register(method, message) - # This utility key function transforms a barsize into a: + # 该工具 key 函数将 barsize 转成: # (Timeframe, Compression) tuple which can be sorted def keyfn(x): n, t = x.split() tf, comp = self._sizes[t] return (tf, int(n) * comp) - # This utility key function transforms a duration into a: + # 该工具 key 函数将 duration 转成: # (Timeframe, Compression) tuple which can be sorted def key2fn(x): n, d = x.split() tf = self._dur2tf[d] return (tf, int(n)) - # Generate a table of reverse durations + # 生成反向 duration 表 self.revdur = collections.defaultdict(list) - # The table (dict) is a ONE to MANY relation of + # 原表(dict)是 ONE to MANY 关系: # duration -> barsizes - # Here it is reversed to get a ONE to MANY relation of + # 这里反转为 ONE to MANY 关系: # barsize -> durations for duration, barsizes in self._durations.items(): for barsize in barsizes: self.revdur[keyfn(barsize)].append(duration) - # Once managed, sort the durations according to real duration and not - # to the text form using the utility key above + # 生成后按真实 duration 排序,而不是按文本形式排序 for barsize in self.revdur: self.revdur[barsize].sort(key=key2fn) def start(self, data=None, broker=None): - self.reconnect(fromstart=True) # reconnect should be an invariant + self.reconnect(fromstart=True) # reconnect 应保持不变式 - # Datas require some processing to kickstart data reception + # data 需要一些处理来启动数据接收 if data is not None: self._env = data._env - # For datas simulate a queue with None to kickstart co + # 对 data 使用带 None 的模拟 queue 启动连接 self.datas.append(data) - # if connection fails, get a fake registration that will force the - # datas to try to reconnect or else bail out + # 如果连接失败,返回一个假注册,强制 data 尝试重连或退出 return self.getTickerQueue(start=True) elif broker is not None: @@ -315,67 +269,59 @@ def start(self, data=None, broker=None): def stop(self): try: - self.conn.disconnect() # disconnect should be an invariant + self.conn.disconnect() # disconnect 应保持不变式 except AttributeError: - pass # conn may have never been connected and lack "disconnect" + pass # conn 可能从未连接,因此没有 "disconnect" - # Unblock any calls set on these events + # 解除等待这些 event 的调用 self._event_managed_accounts.set() self._event_accdownload.set() def logmsg(self, *args): - # for logging purposes + # 用于 logging if self.p._debug: print(*args) def watcher(self, msg): - # will be registered to see all messages if debug is requested + # 请求 debug 时注册,用于观察所有 message self.logmsg(str(msg)) if self.p.notifyall: self.notifs.put((msg, tuple(msg.values()), dict(msg.items()))) def connected(self): - # The isConnected method is available through __getattr__ indirections - # and may not be present, which indicates that no connection has been - # made because the subattribute sender has not yet been created, hence - # the check for the AttributeError exception + # isConnected 通过 __getattr__ 间接提供,可能不存在。 + # 不存在表示尚未创建子属性 sender,即尚未建立连接,因此需要捕获 AttributeError。 try: return self.conn.isConnected() except AttributeError: pass - return False # non-connected (including non-initialized) + return False # 未连接(包括未初始化) def reconnect(self, fromstart=False, resub=False): - # This method must be an invariant in that it can be called several - # times from the same source and must be consistent. An exampler would - # be 5 datas which are being received simultaneously and all request a - # reconnect - - # Policy: - # - if dontreconnect has been set, no option to connect is possible - # - check connection and use the absence of isConnected as signal of - # first ever connection (add 1 to retries too) - # - Calculate the retries (forever or not) - # - Try to connct - # - If achieved and fromstart is false, the datas will be - # re-kickstarted to recreate the subscription + # 该方法必须保持不变式:同一来源可多次调用,结果必须一致。 + # 例如 5 个 data 同时接收并同时请求 reconnect。 + + # 策略: + # - 如果 dontreconnect 已设置,则不再尝试连接 + # - 检查连接;缺少 isConnected 表示首次连接(retries 也加 1) + # - 计算 retries(永久或有限次数) + # - 尝试连接 + # - 如果成功且 fromstart 为 False,则重新启动 data 以重建 subscription firstconnect = False try: if self.conn.isConnected(): if resub: self.startdatas() - return True # nothing to do + return True # 无需操作 except AttributeError: - # Not connected, several __getattr__ indirections to - # self.conn.sender.client.isConnected + # 未连接,需要通过多层 __getattr__ 间接访问 self.conn.sender.client.isConnected firstconnect = True if self.dontreconnect: return False - # This is only invoked from the main thread by datas and therefore no - # lock is needed to control synchronicity to it + # 该方法只由 data 在主线程中调用,因此无需加锁控制同步 retries = self.p.reconnect if retries >= 0: retries += firstconnect @@ -389,16 +335,16 @@ def reconnect(self, fromstart=False, resub=False): if self.conn.connect(): if not fromstart or resub: self.startdatas() - return True # connection successful + return True # 连接成功 if retries > 0: retries -= 1 self.dontreconnect = True - return False # connection/reconnection failed + return False # 连接/重连失败 def startdatas(self): - # kickstrat datas, not returning until all of them have been done + # 启动 data,直到全部完成后才返回 ts = list() for data in self.datas: t = threading.Thread(target=data.reqdata) @@ -409,7 +355,7 @@ def startdatas(self): t.join() def stopdatas(self): - # stop subs and force datas out of the loop (in LIFO order) + # 停止 subscription,并按 LIFO 顺序强制 data 退出循环 qs = list(self.qs.values()) ts = list() for data in self.datas: @@ -420,18 +366,17 @@ def stopdatas(self): for t in ts: t.join() - for q in reversed(qs): # datamaster the last one to get a None + for q in reversed(qs): # datamaster 最后收到 None q.put(None) def get_notifications(self): - '''Return the pending "store" notifications''' - # The background thread could keep on adding notifications. The None - # mark allows to identify which is the last notification to deliver - self.notifs.put(None) # put a mark + '''返回待处理的 "store" 通知。''' + # 后台线程可能持续添加 notification。None 标记用于识别本次要发送的最后一个 notification。 + self.notifs.put(None) # 放置标记 notifs = list() while True: notif = self.notifs.get() - if notif is None: # mark is reached + if notif is None: # 到达标记 break notifs.append(notif) @@ -449,108 +394,103 @@ def error(self, msg): # 1300- Socket dropped in client-TWS communication # 2100-2110 Informative about Data Farm status (id=-1) - # All errors are logged to the environment (cerebro), because many - # errors in Interactive Brokers are actually informational and many may - # actually be of interest to the user + # 所有 error 都记录到 environment(cerebro),因为 IB 的很多 error 实际上是信息性消息, + # 且其中不少可能对用户有价值。 if not self.p.notifyall: self.notifs.put((msg, tuple(msg.values()), dict(msg.items()))) - # Manage those events which have to do with connection + # 管理与连接相关的 event if msg.errorCode is None: - # Usually received as an error in connection of just before disconn + # 通常在连接出错或即将断开前收到 pass elif msg.errorCode in [200, 203, 162, 320, 321, 322]: - # cdetails 200 security not found, notify over right queue + # cdetails 200 security not found,通过对应 queue 通知 # cdetails 203 security not allowed for acct try: q = self.qs[msg.id] except KeyError: - pass # should not happend but it can + pass # 理论上不应发生,但可能发生 else: self.cancelQueue(q, True) elif msg.errorCode in [354, 420]: - # 354 no subscription, 420 no real-time bar for contract - # the calling data to let the data know ... it cannot resub + # 354 no subscription,420 no real-time bar for contract。 + # 通知调用 data,让 data 知道不能 resub。 try: q = self.qs[msg.id] except KeyError: - pass # should not happend but it can + pass # 理论上不应发生,但可能发生 else: q.put(-msg.errorCode) self.cancelQueue(q) elif msg.errorCode == 10225: - # 10225-Bust event occurred, current subscription is deactivated. - # Please resubscribe real-time bars immediately. + # 10225-Bust event occurred,当前 subscription 已停用,需要立即重新订阅 real-time bars。 try: q = self.qs[msg.id] except KeyError: - pass # should not happend but it can + pass # 理论上不应发生,但可能发生 else: q.put(-msg.errorCode) - elif msg.errorCode == 326: # not recoverable, clientId in use + elif msg.errorCode == 326: # 不可恢复,clientId 已被使用 self.dontreconnect = True self.conn.disconnect() self.stopdatas() elif msg.errorCode == 502: - # Cannot connect to TWS: port, config not open, tws off (504 then) + # 无法连接 TWS:端口/配置未打开,或 TWS 关闭(随后可能出现 504) self.conn.disconnect() self.stopdatas() - elif msg.errorCode == 504: # Not Connected for data op - # Once for each data - pass # don't need to manage it + elif msg.errorCode == 504: # data 操作时未连接 + # 每个 data 可能各出现一次 + pass # 无需处理 elif msg.errorCode == 1300: - # TWS has been closed. The port for a new connection is there + # TWS 已关闭。新连接端口包含在消息中 # newport = int(msg.errorMsg.split('-')[-1]) # bla bla bla -7496 self.conn.disconnect() self.stopdatas() elif msg.errorCode == 1100: - # Connection lost - Notify ... datas will wait on the queue - # with no messages arriving + # 连接丢失,通知 data;data 会在 queue 上等待但不会收到消息 for q in self.ts: # key: queue -> ticker q.put(-msg.errorCode) elif msg.errorCode == 1101: - # Connection restored and tickerIds are gone + # 连接恢复,但 tickerId 已丢失 for q in self.ts: # key: queue -> ticker q.put(-msg.errorCode) elif msg.errorCode == 1102: - # Connection restored and tickerIds maintained + # 连接恢复,tickerId 保持有效 for q in self.ts: # key: queue -> ticker q.put(-msg.errorCode) elif msg.errorCode < 500: - # Given the myriad of errorCodes, start by assuming is an order - # error and if not, the checks there will let it go + # errorCode 类型很多,先假设它是 order error;若不是,后续检查会放过它 if msg.id < self.REQIDBASE: if self.broker is not None: self.broker.push_ordererror(msg) else: - # Cancel the queue if a "data" reqId error is given: sanity + # 如果给出 "data" reqId error,则取消 queue,属于 sanity 处理 q = self.qs[msg.id] self.cancelQueue(q, True) @ibregister def connectionClosed(self, msg): - # Sometmes this comes without 1300/502 or any other and will not be - # seen in error hence the need to manage the situation independently + # 有时该事件不伴随 1300/502 或其他 error,因此需要独立处理 self.conn.disconnect() self.stopdatas() @ibregister def managedAccounts(self, msg): - # 1st message in the stream + # stream 中的第 1 条消息 self.managed_accounts = msg.accountsList.split(',') self._event_managed_accounts.set() - # Request time to avoid synchronization issues + # 请求时间以避免同步问题 self.reqCurrentTime() def reqCurrentTime(self): @@ -558,7 +498,7 @@ def reqCurrentTime(self): @ibregister def currentTime(self, msg): - if not self.p.timeoffset: # only if requested ... apply timeoffset + if not self.p.timeoffset: # 仅在请求时应用 timeoffset return curtime = datetime.fromtimestamp(float(msg.time)) with self._lock_tmoffset: @@ -571,36 +511,35 @@ def timeoffset(self): return self.tmoffset def nextTickerId(self): - # Get the next ticker using next on the itertools.count + # 通过 itertools.count 获取下一个 ticker return next(self._tickerId) @ibregister def nextValidId(self, msg): - # Create a counter from the TWS notified value to apply to orders + # 从 TWS 通知值创建 counter,用于 order self.orderid = itertools.count(msg.orderId) def nextOrderId(self): - # Get the next ticker using next on the itertools.count made with the - # notified value from TWS + # 从基于 TWS 通知值创建的 itertools.count 获取下一个 order id return next(self.orderid) def reuseQueue(self, tickerId): - '''Reuses queue for tickerId, returning the new tickerId and q''' + '''为 tickerId 复用 queue,并返回新的 tickerId 与 q。''' with self._lock_q: - # Invalidate tickerId in qs (where it is a key) - q = self.qs.pop(tickerId, None) # invalidate old + # 在 qs 中使 tickerId 失效(它是 key) + q = self.qs.pop(tickerId, None) # 使旧值失效 iscash = self.iscash.pop(tickerId, None) - # Update ts: q -> ticker - tickerId = self.nextTickerId() # get new tickerId - self.ts[q] = tickerId # Update ts: q -> tickerId - self.qs[tickerId] = q # Update qs: tickerId -> q + # 更新 ts: q -> ticker + tickerId = self.nextTickerId() # 获取新的 tickerId + self.ts[q] = tickerId # 更新 ts: q -> tickerId + self.qs[tickerId] = q # 更新 qs: tickerId -> q self.iscash[tickerId] = iscash return tickerId, q def getTickerQueue(self, start=False): - '''Creates ticker/Queue for data delivery to a data feed''' + '''创建用于向 data feed 传递数据的 ticker/Queue。''' q = queue.Queue() if start: q.put(None) @@ -608,15 +547,15 @@ def getTickerQueue(self, start=False): with self._lock_q: tickerId = self.nextTickerId() - self.qs[tickerId] = q # can be managed from other thread + self.qs[tickerId] = q # 可由其他线程管理 self.ts[q] = tickerId self.iscash[tickerId] = False return tickerId, q def cancelQueue(self, q, sendnone=False): - '''Cancels a Queue for data delivery''' - # pop ts (tickers) and with the result qs (queues) + '''取消用于数据传递的 Queue。''' + # pop ts(tickers),并用结果 pop qs(queues) tickerId = self.ts.pop(q, None) self.qs.pop(tickerId, None) @@ -626,7 +565,7 @@ def cancelQueue(self, q, sendnone=False): q.put(None) def validQueue(self, q): - '''Returns (bool) if a queue is still valid''' + '''返回 queue 是否仍然有效。''' return q in self.ts # queue -> ticker def getContractDetails(self, contract, maxcount=None): @@ -646,19 +585,19 @@ def getContractDetails(self, contract, maxcount=None): return cds def reqContractDetails(self, contract): - # get a ticker/queue for identification/data delivery + # 获取 ticker/queue,用于识别和数据传递 tickerId, q = self.getTickerQueue() self.conn.reqContractDetails(tickerId, contract) return q @ibregister def contractDetailsEnd(self, msg): - '''Signal end of contractdetails''' + '''标记 contractdetails 结束。''' self.cancelQueue(self.qs[msg.reqId], True) @ibregister def contractDetails(self, msg): - '''Receive answer and pass it to the queue''' + '''接收响应并传入 queue。''' self.qs[msg.reqId].put(msg) def reqHistoricalDataEx(self, contract, enddate, begindate, @@ -666,18 +605,18 @@ def reqHistoricalDataEx(self, contract, enddate, begindate, what=None, useRTH=False, tz='', sessionend=None, tickerId=None): ''' - Extension of the raw reqHistoricalData proxy, which takes two dates - rather than a duration, barsize and date + raw reqHistoricalData proxy 的扩展版本,接收两个日期,而不是 duration、 + barsize 和 date。 - It uses the IB published valid duration/barsizes to make a mapping and - spread a historical request over several historical requests if needed + 它使用 IB 发布的有效 duration/barsize 建立映射,并在需要时将一个 historical + request 拆成多个 request。 ''' - # Keep a copy for error reporting purposes + # 保留一份副本,用于 error reporting kwargs = locals().copy() - kwargs.pop('self', None) # remove self, no need to report it + kwargs.pop('self', None) # 移除 self,无需报告 if timeframe < TimeFrame.Seconds: - # Ticks are not supported + # 不支持 ticks return self.getTickerQueue(start=True) if enddate is None: @@ -702,30 +641,30 @@ def reqHistoricalDataEx(self, contract, enddate, begindate, what=what, useRTH=useRTH, tz=tz, sessionend=sessionend) - # Check if the requested timeframe/compression is supported by IB + # 检查请求的 timeframe/compression 是否被 IB 支持 durations = self.getdurations(timeframe, compression) - if not durations: # return a queue and put a None in it + if not durations: # 返回一个 queue,并放入 None return self.getTickerQueue(start=True) - # Get or reuse a queue + # 获取或复用 queue if tickerId is None: tickerId, q = self.getTickerQueue() else: tickerId, q = self.reuseQueue(tickerId) # reuse q for old tickerId - # Get the best possible duration to reduce number of requests + # 获取最佳 duration,以减少 request 数量 duration = None for dur in durations: intdate = self.dt_plus_duration(begindate, dur) if intdate >= enddate: intdate = enddate - duration = dur # begin -> end fits in single request + duration = dur # begin -> end 可放入单个 request break - if duration is None: # no duration large enough to fit the request + if duration is None: # 没有足够大的 duration 覆盖 request duration = durations[-1] - # Store the calculated data + # 保存计算出的数据 self.histexreq[tickerId] = dict( contract=contract, enddate=enddate, begindate=intdate, timeframe=timeframe, compression=compression, @@ -739,7 +678,7 @@ def reqHistoricalDataEx(self, contract, enddate, begindate, if contract.m_secType in ['CASH', 'CFD']: self.iscash[tickerId] = 1 # msg.field code if not what: - what = 'BID' # default for cash unless otherwise specified + what = 'BID' # cash 默认值,除非另行指定 elif contract.m_secType in ['IND'] and self.p.indcash: self.iscash[tickerId] = 4 # msg.field code @@ -754,27 +693,27 @@ def reqHistoricalDataEx(self, contract, enddate, begindate, bytes(barsize), bytes(what), int(useRTH), - 2) # dateformat 1 for string, 2 for unix time in seconds + 2) # dateformat 1 表示 string,2 表示 unix time seconds return q def reqHistoricalData(self, contract, enddate, duration, barsize, what=None, useRTH=False, tz='', sessionend=None): - '''Proxy to reqHistorical Data''' + '''reqHistoricalData 的 proxy。''' - # get a ticker/queue for identification/data delivery + # 获取 ticker/queue,用于识别和数据传递 tickerId, q = self.getTickerQueue() if contract.m_secType in ['CASH', 'CFD']: self.iscash[tickerId] = True if not what: - what = 'BID' # TRADES doesn't work + what = 'BID' # TRADES 不可用 elif what == 'ASK': self.iscash[tickerId] = 2 else: what = what or 'TRADES' - # split barsize "x time", look in sizes for (tf, comp) get tf + # 拆分 barsize "x time",在 sizes 中查找 (tf, comp) 并得到 tf tframe = self._sizes[barsize.split()[1]][0] self.histfmt[tickerId] = tframe >= TimeFrame.Days self.histsend[tickerId] = sessionend @@ -793,30 +732,30 @@ def reqHistoricalData(self, contract, enddate, duration, barsize, return q def cancelHistoricalData(self, q): - '''Cancels an existing HistoricalData request + '''取消已有 HistoricalData request。 - Params: - - q: the Queue returned by reqMktData + Args: + q: reqMktData 返回的 Queue。 ''' with self._lock_q: self.conn.cancelHistoricalData(self.ts[q]) self.cancelQueue(q, True) def reqRealTimeBars(self, contract, useRTH=False, duration=5): - '''Creates a request for (5 seconds) Real Time Bars + '''创建(5 秒)Real Time Bars request。 - Params: - - contract: a ib.ext.Contract.Contract intance - - useRTH: (default: False) passed to TWS - - duration: (default: 5) passed to TWS, no other value works in 2016) + Args: + contract: ib.ext.Contract.Contract 实例。 + useRTH: 传给 TWS。 + duration: 传给 TWS;2016 年只有 5 可用。 Returns: - - a Queue the client can wait on to receive a RTVolume instance + Queue: 客户端可等待该 queue 接收 RTVolume 实例。 ''' - # get a ticker/queue for identification/data delivery + # 获取 ticker/queue,用于识别和数据传递 tickerId, q = self.getTickerQueue() - # 20150929 - Only 5 secs supported for duration + # 2015-09-29:duration 只支持 5 秒 self.conn.reqRealTimeBars( tickerId, contract, @@ -827,10 +766,10 @@ def reqRealTimeBars(self, contract, useRTH=False, duration=5): return q def cancelRealTimeBars(self, q): - '''Cancels an existing MarketData subscription + '''取消已有 MarketData subscription。 - Params: - - q: the Queue returned by reqMktData + Args: + q: reqMktData 返回的 Queue。 ''' with self._lock_q: tickerId = self.ts.get(q, None) @@ -840,34 +779,34 @@ def cancelRealTimeBars(self, q): self.cancelQueue(q, True) def reqMktData(self, contract, what=None): - '''Creates a MarketData subscription + '''创建 MarketData subscription。 - Params: - - contract: a ib.ext.Contract.Contract intance + Args: + contract: ib.ext.Contract.Contract 实例。 Returns: - - a Queue the client can wait on to receive a RTVolume instance + Queue: 客户端可等待该 queue 接收 RTVolume 实例。 ''' - # get a ticker/queue for identification/data delivery + # 获取 ticker/queue,用于识别和数据传递 tickerId, q = self.getTickerQueue() - ticks = '233' # request RTVOLUME tick delivered over tickString + ticks = '233' # 请求通过 tickString 传递的 RTVOLUME tick if contract.m_secType in ['CASH', 'CFD']: self.iscash[tickerId] = True - ticks = '' # cash markets do not get RTVOLUME + ticks = '' # cash market 不接收 RTVOLUME if what == 'ASK': self.iscash[tickerId] = 2 - # q.put(None) # to kickstart backfilling - # Can request 233 also for cash ... nothing will arrive + # q.put(None) # 用于启动 backfilling + # cash 也可请求 233,但不会收到任何内容 self.conn.reqMktData(tickerId, contract, bytes(ticks), False) return q def cancelMktData(self, q): - '''Cancels an existing MarketData subscription + '''取消已有 MarketData subscription。 - Params: - - q: the Queue returned by reqMktData + Args: + q: reqMktData 返回的 Queue。 ''' with self._lock_q: tickerId = self.ts.get(q, None) @@ -878,37 +817,31 @@ def cancelMktData(self, q): @ibregister def tickString(self, msg): - # Receive and process a tickString message + # 接收并处理 tickString message if msg.tickType == 48: # RTVolume try: rtvol = RTVolume(msg.value) except ValueError: # price not in message ... pass else: - # Don't need to adjust the time, because it is in "timestamp" - # form in the message + # 无需调整时间,因为 message 中已经是 timestamp 形式 self.qs[msg.tickerId].put(rtvol) @ibregister def tickPrice(self, msg): - '''Cash Markets have no notion of "last_price"/"last_size" and the - tracking of the price is done (industry de-facto standard at least with - the IB API) following the BID price + '''Cash Market 没有 "last_price"/"last_size" 概念,价格跟踪按 BID price 进行。 - A RTVolume which will only contain a price is put into the client's - queue to have a consistent cross-market interface + 为保持跨市场 interface 一致,会将只包含 price 的 RTVolume 放入客户端 queue。 ''' - # Used for "CASH" markets - # The price field has been seen to be missing in some instances even if - # "field" is 1 + # 用于 "CASH" market + # 曾观察到即便 "field" 为 1,price 字段也可能缺失 tickerId = msg.tickerId fieldcode = self.iscash[tickerId] if fieldcode: if msg.field == fieldcode: # Expected cash field code try: if msg.price == -1.0: - # seems to indicate the stream is halted for example in - # between 23:00 - 23:15 CET for FOREX + # 似乎表示 stream 暂停,例如 FOREX 在 CET 23:00 - 23:15 之间 return except AttributeError: pass @@ -923,21 +856,19 @@ def tickPrice(self, msg): @ibregister def realtimeBar(self, msg): - '''Receives x seconds Real Time Bars (at the time of writing only 5 - seconds are supported) + '''接收 x 秒 Real Time Bars(编写时仅支持 5 秒)。 - Not valid for cash markets + 不适用于 cash market。 ''' - # Get a naive localtime object + # 获取 naive localtime 对象 msg.time = datetime.utcfromtimestamp(float(msg.time)) self.qs[msg.reqId].put(msg) @ibregister def historicalData(self, msg): - '''Receives the events of a historical data request''' - # For multi-tiered downloads we'd need to rebind the queue to a new - # tickerId (in case tickerIds are not reusable) and instead of putting - # None, issue a new reqHistData with the new data and move formward + '''接收 historical data request 的 event。''' + # 对多层下载,需要将 queue 重新绑定到新的 tickerId(以防 tickerId 不可复用), + # 并发起新的 reqHistData,而不是放入 None。 tickerId = msg.reqId q = self.qs[tickerId] if msg.date.startswith('finished-'): @@ -961,10 +892,8 @@ def historicalData(self, msg): if tz: dteostz = tz.localize(dteos) dteosutc = dteostz.astimezone(UTC).replace(tzinfo=None) - # When requesting for example daily bars, the current day - # will be returned with the already happened data. If the - # session end were added, the new ticks wouldn't make it - # through, because they happen before the end of time + # 例如请求 daily bars 时,当前日会带着已发生数据返回。 + # 如果加入 session end,新 tick 因发生在结束时间之前而无法通过。 else: dteosutc = dteos @@ -977,10 +906,8 @@ def historicalData(self, msg): q.put(msg) - # The _durations are meant to calculate the needed historical data to - # perform backfilling at the start of a connetion or a connection is lost. - # Using a timedelta as a key allows to quickly find out which bar size - # bar size (values in the tuples int the dict) can be used. + # _durations 用于计算连接启动或连接丢失后 backfilling 所需的 historical data。 + # 使用 timedelta 作为 key,可快速找出可用 bar size。 _durations = dict([ # 60 seconds - 1 min @@ -1109,8 +1036,7 @@ def historicalData(self, msg): ('1 Y', ('1 day', '1 W', '1 M')), ]) - # Sizes allow for quick translation from bar sizes above to actual - # timeframes to make a comparison with the actual data + # Sizes 用于将上方 bar size 快速转换到实际 timeframe,以便与实际 data 对比 _sizes = { 'secs': (TimeFrame.Seconds, 1), 'min': (TimeFrame.Minutes, 1), @@ -1169,7 +1095,7 @@ def tfcomp_to_size(self, timeframe, compression): if timeframe == TimeFrame.Seconds: return '{} secs'.format(compression) - # Microseconds or ticks + # Microseconds 或 ticks return None def dt_plus_duration(self, dt, duration): @@ -1195,7 +1121,7 @@ def dt_plus_duration(self, dt, duration): return dt # could do nothing with it ... return it intact def calcdurations(self, dtbegin, dtend): - '''Calculate a duration in between 2 datetimes''' + '''计算两个 datetime 之间的 duration。''' duration = self.histduration(dtbegin, dtend) if duration[-1] == 'M': @@ -1212,13 +1138,12 @@ def calcdurations(self, dtbegin, dtend): return duration, sizes def calcduration(self, dtbegin, dtend): - '''Calculate a duration in between 2 datetimes. Returns single size''' + '''计算两个 datetime 之间的 duration,并返回单个 size。''' duration, sizes = self._calcdurations(dtbegin, dtend) return duration, sizes[0] def histduration(self, dt1, dt2): - # Given two dates calculates the smallest possible duration according - # to the table from the Historical Data API limitations provided by IB + # 给定两个日期,根据 IB Historical Data API 限制表计算最小可用 duration # # Seconds: 'x S' (x: [60, 120, 180, 300, 600, 900, 1200, 1800, 3600, # 7200, 10800, 14400, 28800]) @@ -1227,9 +1152,9 @@ def histduration(self, dt1, dt2): # Months: 'x M' (x: [1, 11]) # Years: 'x Y' (x: [1]) - td = dt2 - dt1 # get a timedelta for calculations + td = dt2 - dt1 # 获取 timedelta 用于计算 - # First: array of secs + # 第一阶段:seconds 数组 tsecs = td.total_seconds() secs = [60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 7200, 10800, 14400, 28800] @@ -1240,25 +1165,25 @@ def histduration(self, dt1, dt2): tdextra = bool(td.seconds or td.microseconds) # over days/weeks - # Next: 1 or 2 days + # 下一阶段:1 或 2 天 days = td.days + tdextra if td.days <= 2: return '{} D'.format(days) - # Next: 1 or 2 weeks + # 下一阶段:1 或 2 周 weeks, d = divmod(td.days, 7) weeks += bool(d or tdextra) if weeks <= 2: return '{} W'.format(weeks) - # Get references to dt components + # 获取 dt 组件引用 y2, m2, d2 = dt2.year, dt2.month, dt2.day y1, m1, d1 = dt1.year, dt1.month, dt2.day H2, M2, S2, US2 = dt2.hour, dt2.minute, dt2.second, dt2.microsecond H1, M1, S1, US1 = dt1.hour, dt1.minute, dt1.second, dt1.microsecond - # Next: 1 -> 11 months (11 incl) + # 下一阶段:1 -> 11 个月(含 11) months = (y2 * 12 + m2) - (y1 * 12 + m1) + ( (d2, H2, M2, S2, US2) > (d1, H1, M1, S1, US1)) if months <= 1: # months <= 11 @@ -1266,15 +1191,15 @@ def histduration(self, dt1, dt2): elif months <= 11: return '2 M' # cap at 2 months to keep the table clean - # Next: years + # 下一阶段:years # y = y2 - y1 + (m2, d2, H2, M2, S2, US2) > (m1, d1, H1, M1, S1, US1) # return '{} Y'.format(y) - return '1 Y' # to keep the table clean + return '1 Y' # 保持表简洁 def makecontract(self, symbol, sectype, exch, curr, expiry='', strike=0.0, right='', mult=1): - '''returns a contract from the parameters without check''' + '''不做检查,直接根据参数返回 contract。''' contract = Contract() contract.m_symbol = bytes(symbol) @@ -1292,47 +1217,46 @@ def makecontract(self, symbol, sectype, exch, curr, return contract def cancelOrder(self, orderid): - '''Proxy to cancelOrder''' + '''cancelOrder 的 proxy。''' self.conn.cancelOrder(orderid) def placeOrder(self, orderid, contract, order): - '''Proxy to placeOrder''' + '''placeOrder 的 proxy。''' self.conn.placeOrder(orderid, contract, order) @ibregister def openOrder(self, msg): - '''Receive the event ``openOrder`` events''' + '''接收 ``openOrder`` event。''' self.broker.push_orderstate(msg) @ibregister def execDetails(self, msg): - '''Receive execDetails''' + '''接收 execDetails。''' self.broker.push_execution(msg.execution) @ibregister def orderStatus(self, msg): - '''Receive the event ``orderStatus``''' + '''接收 ``orderStatus`` event。''' self.broker.push_orderstatus(msg) @ibregister def commissionReport(self, msg): - '''Receive the event commissionReport''' + '''接收 commissionReport event。''' self.broker.push_commissionreport(msg.commissionReport) def reqPositions(self): - '''Proxy to reqPositions''' + '''reqPositions 的 proxy。''' self.conn.reqPositions() @ibregister def position(self, msg): - '''Receive event positions''' + '''接收 positions event。''' pass # Not implemented yet def reqAccountUpdates(self, subscribe=True, account=None): - '''Proxy to reqAccountUpdates + '''reqAccountUpdates 的 proxy。 - If ``account`` is ``None``, wait for the ``managedAccounts`` message to - set the account codes + 如果 ``account`` 为 ``None``,会等待 ``managedAccounts`` message 设置 account code。 ''' if account is None: self._event_managed_accounts.wait() @@ -1342,9 +1266,8 @@ def reqAccountUpdates(self, subscribe=True, account=None): @ibregister def accountDownloadEnd(self, msg): - # Signals the end of an account update - # the event indicates it's over. It's only false once, and can be used - # to find out if it has at least been downloaded once + # 标记 account update 结束。 + # 该 event 表示下载已结束。它只会 false 一次,可用于判断是否至少下载过一次。 self._event_accdownload.set() if False: if self.port_update: @@ -1354,8 +1277,7 @@ def accountDownloadEnd(self, msg): @ibregister def updatePortfolio(self, msg): - # Lock access to the position dicts. This is called in sub-thread and - # can kick in at any time + # 锁定 position dict 访问。该方法在子线程中调用,可能随时触发。 with self._lock_pos: if not self._event_accdownload.is_set(): # 1st event seen position = Position(msg.position, msg.averageCost) @@ -1370,13 +1292,12 @@ def updatePortfolio(self, msg): self.notifs.put((err, (), {})) - # Flag signal to broker at the end of account download + # 在 account download 结束时标记发送给 broker 的 signal # self.port_update = True self.broker.push_portupdate() def getposition(self, contract, clone=False): - # Lock access to the position dicts. This is called from main thread - # and updates could be happening in the background + # 锁定 position dict 访问。该方法由主线程调用,同时后台可能有更新。 with self._lock_pos: position = self.positions[contract.m_conId] if clone: @@ -1386,8 +1307,7 @@ def getposition(self, contract, clone=False): @ibregister def updateAccountValue(self, msg): - # Lock access to the dicts where values are updated. This happens in a - # sub-thread and could kick it at anytime + # 锁定 value 更新 dict。该方法在子线程中调用,可能随时触发。 with self._lock_accupd: try: value = float(msg.value) @@ -1397,29 +1317,26 @@ def updateAccountValue(self, msg): self.acc_upds[msg.accountName][msg.key][msg.currency] = value if msg.key == 'NetLiquidation': - # NetLiquidationByCurrency and currency == 'BASE' is the same + # NetLiquidationByCurrency 且 currency == 'BASE' 时含义相同 self.acc_value[msg.accountName] = value elif msg.key == 'TotalCashBalance' and msg.currency == 'BASE': self.acc_cash[msg.accountName] = value def get_acc_values(self, account=None): - '''Returns all account value infos sent by TWS during regular updates - Waits for at least 1 successful download + '''返回 TWS 在常规更新中发送的所有 account value 信息。 - If ``account`` is ``None`` then a dictionary with accounts as keys will - be returned containing all accounts + 至少等待 1 次成功下载。 - If account is specified or the system has only 1 account the dictionary - corresponding to that account is returned + 如果 ``account`` 为 ``None``,返回以 account 为 key 的所有 account 字典。 + 如果指定 account,或系统只有 1 个 account,则返回该 account 对应的字典。 ''' - # Wait for at least 1 account update download to have been finished - # before the account infos can be returned to the calling client + # 至少等待 1 次 account update download 完成后,再向调用方返回 account 信息 if self.connected(): self._event_accdownload.wait() - # Lock access to acc_cash to avoid an event intefering + # 锁定 acc_cash 访问,避免 event 干扰 with self._updacclock: if account is None: - # wait for the managedAccount Messages + # 等待 managedAccount message if self.connected(): self._event_managed_accounts.wait() @@ -1429,7 +1346,7 @@ def get_acc_values(self, account=None): elif len(self.managed_accounts) > 1: return self.acc_upds.copy() - # Only 1 account, fall through to return only 1 + # 只有 1 个 account,继续向下返回单个 account account = self.managed_accounts[0] try: @@ -1440,23 +1357,20 @@ def get_acc_values(self, account=None): return self.acc_upds.copy() def get_acc_value(self, account=None): - '''Returns the net liquidation value sent by TWS during regular updates - Waits for at least 1 successful download + '''返回 TWS 在常规更新中发送的 net liquidation value。 - If ``account`` is ``None`` then a dictionary with accounts as keys will - be returned containing all accounts + 至少等待 1 次成功下载。 - If account is specified or the system has only 1 account the dictionary - corresponding to that account is returned + 如果 ``account`` 为 ``None``,多 account 时返回总和;如果指定 account, + 或系统只有 1 个 account,则返回该 account 对应值。 ''' - # Wait for at least 1 account update download to have been finished - # before the value can be returned to the calling client + # 至少等待 1 次 account update download 完成后,再向调用方返回 value if self.connected(): self._event_accdownload.wait() - # Lock access to acc_cash to avoid an event intefering + # 锁定 acc_cash 访问,避免 event 干扰 with self._lock_accupd: if account is None: - # wait for the managedAccount Messages + # 等待 managedAccount message if self.connected(): self._event_managed_accounts.wait() @@ -1466,7 +1380,7 @@ def get_acc_value(self, account=None): elif len(self.managed_accounts) > 1: return sum(self.acc_value.values()) - # Only 1 account, fall through to return only 1 + # 只有 1 个 account,继续向下返回单个 account account = self.managed_accounts[0] try: @@ -1477,23 +1391,20 @@ def get_acc_value(self, account=None): return float() def get_acc_cash(self, account=None): - '''Returns the total cash value sent by TWS during regular updates - Waits for at least 1 successful download + '''返回 TWS 在常规更新中发送的 total cash value。 - If ``account`` is ``None`` then a dictionary with accounts as keys will - be returned containing all accounts + 至少等待 1 次成功下载。 - If account is specified or the system has only 1 account the dictionary - corresponding to that account is returned + 如果 ``account`` 为 ``None``,多 account 时返回总和;如果指定 account, + 或系统只有 1 个 account,则返回该 account 对应值。 ''' - # Wait for at least 1 account update download to have been finished - # before the cash can be returned to the calling client + # 至少等待 1 次 account update download 完成后,再向调用方返回 cash if self.connected(): self._event_accdownload.wait() - # Lock access to acc_cash to avoid an event intefering + # 锁定 acc_cash 访问,避免 event 干扰 with self._lock_accupd: if account is None: - # wait for the managedAccount Messages + # 等待 managedAccount message if self.connected(): self._event_managed_accounts.wait() @@ -1503,7 +1414,7 @@ def get_acc_cash(self, account=None): elif len(self.managed_accounts) > 1: return sum(self.acc_cash.values()) - # Only 1 account, fall through to return only 1 + # 只有 1 个 account,继续向下返回单个 account account = self.managed_accounts[0] try: diff --git a/backtrader/stores/oandastore.py b/backtrader/stores/oandastore.py index 15396576e..57c201481 100644 --- a/backtrader/stores/oandastore.py +++ b/backtrader/stores/oandastore.py @@ -28,7 +28,7 @@ import threading import oandapy -import requests # oandapy depdendency +import requests # oandapy 依赖 import backtrader as bt from backtrader.metabase import MetaParams @@ -36,7 +36,7 @@ from backtrader.utils import AutoDict -# Extend the exceptions to support extra cases +# 扩展异常以支持额外场景 class OandaRequestError(oandapy.OandaError): def __init__(self): @@ -64,8 +64,8 @@ def __init__(self): class API(oandapy.API): def request(self, endpoint, method='GET', params=None): - # Overriden to make something sensible out of a - # request.RequestException rather than simply issuing a print(str(e)) + # 覆盖默认逻辑,将 request.RequestException 转成可处理响应, + # 而不是仅执行 print(str(e)) url = '%s/%s' % (self.api_url, endpoint) method = method.lower() @@ -79,7 +79,7 @@ def request(self, endpoint, method='GET', params=None): else: request_args['data'] = params - # Added the try block + # 增加异常捕获 try: response = func(url, **request_args) except requests.RequestException as e: @@ -88,9 +88,9 @@ def request(self, endpoint, method='GET', params=None): content = response.content.decode('utf-8') content = json.loads(content) - # error message + # 错误消息 if response.status_code >= 400: - # changed from raise to return + # 从 raise 改为 return return oandapy.OandaError(content).error_response return content @@ -98,7 +98,7 @@ def request(self, endpoint, method='GET', params=None): class Streamer(oandapy.Streamer): def __init__(self, q, headers=None, *args, **kwargs): - # Override to provide headers, which is in the standard API interface + # 覆盖以提供 headers,该能力存在于标准 API interface 中 super(Streamer, self).__init__(*args, **kwargs) if headers: @@ -107,8 +107,7 @@ def __init__(self, q, headers=None, *args, **kwargs): self.q = q def run(self, endpoint, params=None): - # Override to better manage exceptions. - # Kept as much as possible close to the original + # 覆盖以更好地管理异常,并尽量保持接近原始实现 self.connected = True params = params or {} @@ -123,7 +122,7 @@ def run(self, endpoint, params=None): url = '%s/%s' % (self.api_url, endpoint) while self.connected: - # Added exception control here + # 在这里增加异常控制 try: response = self.client.get(url, **request_args) except requests.RequestException as e: @@ -132,9 +131,9 @@ def run(self, endpoint, params=None): if response.status_code != 200: self.on_error(response.content) - break # added break here + break # 在这里增加 break - # Changed chunk_size 90 -> None + # chunk_size 从 90 改为 None try: for line in response.iter_lines(chunk_size=None): if not self.connected: @@ -145,7 +144,7 @@ def run(self, endpoint, params=None): if not (ignore_heartbeat and 'heartbeat' in data): self.on_success(data) - except: # socket.error has been seen + except: # 曾观察到 socket.error self.q.put(OandaStreamError().error_response) break @@ -161,7 +160,7 @@ def on_error(self, data): class MetaSingleton(MetaParams): - '''Metaclass to make a metaclassed class a singleton''' + '''让带 metaclass 的类成为 singleton 的 metaclass。''' def __init__(cls, name, bases, dct): super(MetaSingleton, cls).__init__(name, bases, dct) cls._singleton = None @@ -175,28 +174,26 @@ def __call__(cls, *args, **kwargs): class OandaStore(with_metaclass(MetaSingleton, object)): - '''Singleton class wrapping to control the connections to Oanda. + '''控制 Oanda 连接的 singleton store。 - Params: + Args: + token: API access token。 + account: account id。 + practice: 是否使用测试环境。 + account_tmout: account value/cash 刷新周期。 - - ``token`` (default:``None``): API access token - - - ``account`` (default: ``None``): account id - - - ``practice`` (default: ``False``): use the test environment - - - ``account_tmout`` (default: ``10.0``): refresh period for account - value/cash refresh + Returns: + OandaStore: 用于管理 Oanda API、streaming、order 与 account 状态的 store。 ''' - BrokerCls = None # broker class will autoregister - DataCls = None # data class will auto register + BrokerCls = None # broker class 会自动注册 + DataCls = None # data class 会自动注册 params = ( ('token', ''), ('account', ''), ('practice', False), - ('account_tmout', 10.0), # account balance refresh timeout + ('account_tmout', 10.0), # account balance 刷新 timeout ) _DTEPOCH = datetime(1970, 1, 1) @@ -205,25 +202,25 @@ class OandaStore(with_metaclass(MetaSingleton, object)): @classmethod def getdata(cls, *args, **kwargs): - '''Returns ``DataCls`` with args, kwargs''' + '''使用 args/kwargs 返回 ``DataCls`` 实例。''' return cls.DataCls(*args, **kwargs) @classmethod def getbroker(cls, *args, **kwargs): - '''Returns broker with *args, **kwargs from registered ``BrokerCls``''' + '''使用注册的 ``BrokerCls`` 与 args/kwargs 返回 broker。''' return cls.BrokerCls(*args, **kwargs) def __init__(self): super(OandaStore, self).__init__() - self.notifs = collections.deque() # store notifications for cerebro + self.notifs = collections.deque() # 发送给 cerebro 的 store 通知 - self._env = None # reference to cerebro for general notifications - self.broker = None # broker instance - self.datas = list() # datas that have registered over start + self._env = None # 指向 cerebro,用于通用通知 + self.broker = None # broker 实例 + self.datas = list() # start 期间注册的 data - self._orders = collections.OrderedDict() # map order.ref to oid - self._ordersrev = collections.OrderedDict() # map oid to order.ref + self._orders = collections.OrderedDict() # 映射 order.ref -> oid + self._ordersrev = collections.OrderedDict() # 映射 oid -> order.ref self._transpend = collections.defaultdict(collections.deque) self._oenv = self._ENVPRACTICE if self.p.practice else self._ENVLIVE @@ -236,14 +233,14 @@ def __init__(self): self._evt_acct = threading.Event() def start(self, data=None, broker=None): - # Datas require some processing to kickstart data reception + # data 需要一些处理来启动数据接收 if data is None and broker is None: self.cash = None return if data is not None: self._env = data._env - # For datas simulate a queue with None to kickstart co + # 对 data 使用带 None 的模拟 queue 启动连接 self.datas.append(data) if self.broker is not None: @@ -255,7 +252,7 @@ def start(self, data=None, broker=None): self.broker_threads() def stop(self): - # signal end of thread + # 标记线程结束 if self.broker is not None: self.q_ordercreate.put(None) self.q_orderclose.put(None) @@ -265,11 +262,11 @@ def put_notification(self, msg, *args, **kwargs): self.notifs.append((msg, args, kwargs)) def get_notifications(self): - '''Return the pending "store" notifications''' - self.notifs.append(None) # put a mark / threads could still append + '''返回待处理的 "store" 通知。''' + self.notifs.append(None) # 放置标记;线程仍可能继续 append return [x for x in iter(self.notifs.popleft, None)] - # Oanda supported granularities + # Oanda 支持的 granularity _GRANULARITIES = { (bt.TimeFrame.Seconds, 5): 'S5', (bt.TimeFrame.Seconds, 10): 'S10', @@ -386,7 +383,7 @@ def _t_candles(self, dataname, dtbegin, dtend, timeframe, compression, for candle in response.get('candles', []): q.put(candle) - q.put({}) # end of transmission + q.put({}) # 传输结束 def streaming_prices(self, dataname, tmout=None): q = queue.Queue() @@ -421,7 +418,7 @@ def get_value(self): def broker_threads(self): self.q_account = queue.Queue() - self.q_account.put(True) # force an immediate update + self.q_account.put(True) # 强制立即更新 t = threading.Thread(target=self._t_account) t.daemon = True t.start() @@ -436,7 +433,7 @@ def broker_threads(self): t.daemon = True t.start() - # Wait once for the values to be set + # 等待一次,确保值已设置 self._evt_acct.wait(self.p.account_tmout) def _t_account(self): @@ -444,8 +441,8 @@ def _t_account(self): try: msg = self.q_account.get(timeout=self.p.account_tmout) if msg is None: - break # end of thread - except queue.Empty: # tmout -> time to refresh + break # 线程结束 + except queue.Empty: # timeout -> 到刷新时间 pass try: @@ -471,11 +468,11 @@ def order_create(self, order, stopside=None, takeside=None, **kwargs): if order.exectype != bt.Order.Market: okwargs['price'] = order.created.price if order.valid is None: - # 1 year and datetime.max fail ... 1 month works + # 1 年和 datetime.max 会失败;1 个月可用 valid = datetime.utcnow() + timedelta(days=30) else: valid = order.data.num2date(order.valid) - # To timestamp with seconds precision + # 转成秒级 timestamp okwargs['expiry'] = int((valid - self._DTEPOCH).total_seconds()) if order.exectype == bt.Order.StopLimit: @@ -491,7 +488,7 @@ def order_create(self, order, stopside=None, takeside=None, **kwargs): if takeside is not None: okwargs['takeProfit'] = takeside.price - okwargs.update(**kwargs) # anything from the user + okwargs.update(**kwargs) # 用户传入的其他内容 self.q_ordercreate.put((order.ref, okwargs,)) return order @@ -513,8 +510,7 @@ def _t_order_create(self): self.broker._reject(oref) return - # Ids are delivered in different fields and all must be fetched to - # match them (as executions) to the order generated here + # id 会在不同字段中返回,必须全部取出,才能作为 execution 匹配到这里生成的 order oids = list() for oidfield in self._OIDSINGLE: if oidfield in o and 'id' in o[oidfield]: @@ -532,14 +528,14 @@ def _t_order_create(self): self._orders[oref] = oids[0] self.broker._submit(oref) if okwargs['type'] == 'market': - self.broker._accept(oref) # taken immediately + self.broker._accept(oref) # 立即接收 for oid in oids: - self._ordersrev[oid] = oref # maps ids to backtrader order + self._ordersrev[oid] = oref # 映射 id 到 backtrader order - # An transaction may have happened and was stored + # transaction 可能已经发生并被暂存 tpending = self._transpend[oid] - tpending.append(None) # eom marker + tpending.append(None) # eom 标记 while True: trans = tpending.popleft() if trans is None: @@ -558,11 +554,11 @@ def _t_order_cancel(self): oid = self._orders.get(oref, None) if oid is None: - continue # the order is no longer there + continue # order 已不存在 try: o = self.oapi.close_order(self.p.account, oid) except Exception as e: - continue # not cancelled - FIXME: notify + continue # 未取消 - FIXME: 通知 self.broker._cancel(oref) @@ -570,9 +566,8 @@ def _t_order_cancel(self): 'LIMIT_ORDER_CREATE', 'MARKET_IF_TOUCHED_ORDER_CREATE',) def _transaction(self, trans): - # Invoked from Streaming Events. May actually receive an event for an - # oid which has not yet been returned after creating an order. Hence - # store if not yet seen, else forward to processer + # 从 Streaming Events 调用。可能收到某个 oid 的事件,但创建 order 后该 oid 尚未返回。 + # 因此未见过时先暂存,否则转发给处理器。 ttype = trans['type'] if ttype == 'MARKET_ORDER_CREATE': try: @@ -581,7 +576,7 @@ def _transaction(self, trans): try: oid = trans['tradeOpened']['id'] except KeyError: - return # cannot do anything else + return # 无法继续处理 elif ttype in self._X_ORDER_CREATE: oid = trans['id'] @@ -594,20 +589,18 @@ def _transaction(self, trans): elif ttype == 'TRADE_CLOSE': oid = trans['id'] pid = trans['tradeId'] - if pid in self._orders and False: # Know nothing about trade - return # can do nothing - - # Skip above - at the moment do nothing - # Received directly from an event in the WebGUI for example which - # closes an existing position related to order with id -> pid - # COULD BE DONE: Generate a fake counter order to gracefully - # close the existing position + if pid in self._orders and False: # 对 trade 无可用信息 + return # 无法处理 + + # 跳过上方逻辑;当前不做处理。 + # 例如直接从 WebGUI 收到关闭现有 position 的事件,该 position 关联 order id -> pid。 + # 可考虑生成一个假的反向 order,以平滑关闭现有 position。 msg = ('Received TRADE_CLOSE for unknown order, possibly generated' ' over a different client or GUI') self.put_notification(msg, trans) return - else: # Go aways gracefully + else: # 平稳退出未知情况 try: oid = trans['id'] except KeyError: @@ -621,7 +614,7 @@ def _transaction(self, trans): try: oref = self._ordersrev[oid] self._process_transaction(oid, trans) - except KeyError: # not yet seen, keep as pending + except KeyError: # 尚未见过,保留为 pending self._transpend[oid].append(trans) _X_ORDER_FILLED = ('MARKET_ORDER_CREATE', @@ -650,10 +643,10 @@ def _process_transaction(self, oid, trans): elif ttype in 'ORDER_CANCEL': reason = trans['reason'] if reason == 'ORDER_FILLED': - pass # individual execs have done the job + pass # 单个 execution 已完成工作 elif reason == 'TIME_IN_FORCE_EXPIRED': self.broker._expire(oref) elif reason == 'CLIENT_REQUEST': self.broker._cancel(oref) - else: # default action ... if nothing else + else: # 其他情况的默认动作 self.broker._reject(oref) diff --git a/backtrader/stores/vchartfile.py b/backtrader/stores/vchartfile.py index 35ef8ebaf..a16facd74 100644 --- a/backtrader/stores/vchartfile.py +++ b/backtrader/stores/vchartfile.py @@ -27,14 +27,21 @@ class VChartFile(bt.Store): - '''Store provider for Visual Chart binary files + '''Visual Chart 二进制文件的 Store provider。 - Params: + Args: + path: Visual Chart 数据文件目录。若为 ``None`` 且运行于 Windows,会查询注册表 + 以定位 *Visual Chart* 文件根目录。 - - ``path`` (default:``None``): + Returns: + VChartFile: 用于提供 Visual Chart 文件路径的 store。 - If the path is ``None`` and running under *Windows*, the registry will - be examined to find the root directory of the *Visual Chart* files. + --- + 交互界面使用示范: + + >>> store = VChartFile(path='') + >>> store.get_datapath() + '' ''' params = ( @@ -48,8 +55,7 @@ def __init__(self): @staticmethod def _find_vchart(): - # Find VisualChart registry key to get data directory - # If not found returns '' + # 查找 VisualChart 注册表项以获取数据目录;找不到则返回 '' VC_KEYNAME = r'SOFTWARE\VCG\Visual Chart 6\Config' VC_KEYVAL = 'DocsDirectory' VC_DATADIR = ['Realserver', 'Data', '01'] @@ -61,22 +67,22 @@ def _find_vchart(): return VC_NONE vcdir = None - # Search for Directory in the usual root keys + # 在常见根键中搜索目录 for rkey in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE,): try: vckey = winreg.OpenKey(rkey, VC_KEYNAME) except WindowsError as e: continue - # Try to get the key value + # 尝试读取键值 try: vcdir, _ = winreg.QueryValueEx(vckey, VC_KEYVAL) except WindowsError as e: continue else: - break # found vcdir + break # 找到 vcdir - if vcdir is not None: # something was found + if vcdir is not None: # 找到了内容 vcdir = os.path.join(vcdir, *VC_DATADIR) else: vcdir = VC_NONE diff --git a/backtrader/stores/vcstore.py b/backtrader/stores/vcstore.py index 237e0ebc1..fbdbea846 100644 --- a/backtrader/stores/vcstore.py +++ b/backtrader/stores/vcstore.py @@ -39,7 +39,7 @@ class _SymInfo(object): - # Replica of the SymbolInfo COM object to pass it over thread boundaries + # SymbolInfo COM 对象副本,用于跨线程传递 _fields = ['Type', 'Description', 'Decimals', 'TimeOffset', 'PointValue', 'MinMovement'] @@ -47,42 +47,40 @@ def __init__(self, syminfo): for f in self._fields: setattr(self, f, getattr(syminfo, f)) -# This type is used inside 'PumpEvents', but if we create the type -# afresh each time 'PumpEvents' is called we end up creating cyclic -# garbage for each call. So we define it here instead. +# 该类型在 'PumpEvents' 内使用;如果每次调用都重新创建,会为每次调用制造循环垃圾。 +# 因此在这里统一定义。 _handles_type = ctypes.c_void_p * 1 def PumpEvents(timeout=-1, hevt=None, cb=None): - """This following code waits for 'timeout' seconds in the way - required for COM, internally doing the correct things depending - on the COM appartment of the current thread. It is possible to - terminate the message loop by pressing CTRL+C, which will raise - a KeyboardInterrupt. + """按 COM 需要的方式等待 ``timeout`` 秒。 + + 内部会根据当前线程所属 COM apartment 执行相应处理。按 CTRL+C 可终止 message + loop,并抛出 KeyboardInterrupt。 + + Args: + timeout: 等待秒数,或返回等待秒数的 callable;``-1`` 表示无限等待。 + hevt: 可选的 Windows event handle。 + cb: timeout 后可调用的 callback。 """ - # XXX Should there be a way to pass additional event handles which - # can terminate this function? + # XXX 是否应支持传入额外 event handle 来终止该函数? # XXX XXX XXX # - # It may be that I misunderstood the CoWaitForMultipleHandles - # function. Is a message loop required in a STA? Seems so... + # 可能存在对 CoWaitForMultipleHandles 的理解偏差。STA 是否需要 message loop? + # 看起来是需要的。 # - # MSDN says: + # MSDN 说明: # - # If the caller resides in a single-thread apartment, - # CoWaitForMultipleHandles enters the COM modal loop, and the - # thread's message loop will continue to dispatch messages using - # the thread's message filter. If no message filter is registered - # for the thread, the default COM message processing is used. + # 如果调用方位于 single-thread apartment,CoWaitForMultipleHandles 会进入 COM + # modal loop,线程的 message loop 会继续通过 thread message filter 分发消息。 + # 如果线程未注册 message filter,则使用默认 COM message processing。 # - # If the calling thread resides in a multithread apartment (MTA), - # CoWaitForMultipleHandles calls the Win32 function - # MsgWaitForMultipleObjects. + # 如果调用线程位于 multithread apartment (MTA),CoWaitForMultipleHandles 会调用 + # Win32 函数 MsgWaitForMultipleObjects。 - # Timeout expected as float in seconds - *1000 to miliseconds - # timeout = -1 -> INFINITE 0xFFFFFFFF; - # It can also be a callable which should return an amount in seconds + # Timeout 预期为秒数 float,需要 *1000 转为毫秒。 + # timeout = -1 -> INFINITE 0xFFFFFFFF;也可以是返回秒数的 callable。 if hevt is None: hevt = ctypes.windll.kernel32.CreateEventA(None, True, False, None) @@ -118,7 +116,7 @@ def HandlerRoutine(dwCtrlType): int(tmout), # dwtimeout len(handles), # number of handles in handles handles, # handles array - # pointer to indicate which handle was signaled + # 指示哪个 handle 被触发的指针 ctypes.byref(ctypes.c_ulong()) ) @@ -158,20 +156,20 @@ def OnServerShutDown(self): self.store._vcrt_connection(self.store._RT_SHUTDOWN) def OnInternalEvent(self, p1, p2, p3): - if p1 != 1: # Apparently "Connection Event" + if p1 != 1: # 看起来是 "Connection Event" return if p2 == self.lastconn: - return # do not notify twice + return # 不重复通知 - self.lastconn = p2 # keep new notification code + self.lastconn = p2 # 保存新的通知代码 - # p2 should be 0 (disconn), 1 (conn) + # p2 应为 0(disconn)或 1(conn) self.store._vcrt_connection(self.store._RT_BASEMSG - p2) class MetaSingleton(MetaParams): - '''Metaclass to make a metaclassed class a singleton''' + '''让带 metaclass 的类成为 singleton 的 metaclass。''' def __init__(cls, name, bases, dct): super(MetaSingleton, cls).__init__(name, bases, dct) cls._singleton = None @@ -185,19 +183,18 @@ def __call__(cls, *args, **kwargs): class VCStore(with_metaclass(MetaSingleton, object)): - '''Singleton class wrapping an ibpy ibConnection instance. + '''封装 VisualChart COM 连接的 singleton store。 - The parameters can also be specified in the classes which use this store, - like ``VCData`` and ``VCBroker`` + 参数也可在使用该 store 的类中指定,例如 ``VCData`` 和 ``VCBroker``。 ''' - BrokerCls = None # broker class will autoregister - DataCls = None # data class will auto register + BrokerCls = None # broker class 会自动注册 + DataCls = None # data class 会自动注册 - # 32 bit max unsigned int for openinterest correction + # 用于 openinterest 校正的 32 bit 最大无符号整数 MAXUINT = 0xffffffff // 2 - # to remove at least 1 sec or else there seem to be internal conv problems + # 至少减去 1 秒,否则内部转换可能有问题 MAXDATE1 = datetime.max - timedelta(days=1, seconds=1) MAXDATE2 = datetime.max - timedelta(seconds=1) @@ -213,22 +210,22 @@ class VCStore(with_metaclass(MetaSingleton, object)): @classmethod def getdata(cls, *args, **kwargs): - '''Returns ``DataCls`` with args, kwargs''' + '''使用 args/kwargs 返回 ``DataCls`` 实例。''' return cls.DataCls(*args, **kwargs) @classmethod def getbroker(cls, *args, **kwargs): - '''Returns broker with *args, **kwargs from registered ``BrokerCls``''' + '''使用注册的 ``BrokerCls`` 与 args/kwargs 返回 broker。''' return cls.BrokerCls(*args, **kwargs) - # DLLs to parse if found for TypeLibs + # 找到时用于解析 TypeLibs 的 DLL VC64_DLLS = ('VCDataSource64.dll', 'VCRealTimeLib64.dll', 'COMTraderInterfaces64.dll',) VC_DLLS = ('VCDataSource.dll', 'VCRealTimeLib.dll', 'COMTraderInterfaces.dll',) - # Well known CLSDI + # 已知 CLSID VC_TLIBS = ( ['{EB2A77DC-A317-4160-8833-DECF16275A05}', 1, 0], # vcdatasource64 ['{86F1DB04-2591-4866-A361-BB053D77FA18}', 1, 0], # vcrealtime64 @@ -240,37 +237,34 @@ def getbroker(cls, *args, **kwargs): VC_BINPATH = 'bin' def find_vchart(self): - # Tries to locate VisualChart in the registry to get the installation - # directory - # If not found returns well-known typelibs clsid - # Else it will scan the directory to locate the 64/32 bit dlls and - # return the paths + # 尝试在注册表中定位 VisualChart 安装目录。 + # 找不到时返回已知 typelibs clsid;找到时扫描目录定位 64/32 bit DLL 并返回路径。 import _winreg # keep import local to avoid breaking test cases vcdir = None - # Search for Directory in the usual root keys + # 在常见根键中搜索 Directory for rkey in (_winreg.HKEY_CURRENT_USER, _winreg.HKEY_LOCAL_MACHINE,): try: vckey = _winreg.OpenKey(rkey, self.VC_KEYNAME) except WindowsError as e: continue - # Try to get the key value + # 尝试读取键值 try: vcdir, _ = _winreg.QueryValueEx(vckey, self.VC_KEYVAL) except WindowsError as e: continue else: - break # found vcdir + break # 找到 vcdir if vcdir is None: - return self.VC_TLIBS # no dir found, last resort + return self.VC_TLIBS # 未找到目录,最后回退 - # DLLs are in the bin directory + # DLL 位于 bin 目录 vcbin = os.path.join(vcdir, self.VC_BINPATH) - # Search for the 3 libraries (64/32 bits) in the found dir + # 在找到的目录中搜索 3 个库(64/32 bit) for dlls in (self.VC64_DLLS, self.VC_DLLS,): dfound = [] for dll in dlls: @@ -282,11 +276,11 @@ def find_vchart(self): if len(dfound) == len(dlls): return dfound - # not all dlls were found, last resort + # 未找到全部 DLL,最后回退 return self.VC_TLIBS def _load_comtypes(self): - # Keep comtypes imports local to avoid breaking testcases + # 将 comtypes import 保持为局部操作,避免破坏测试用例 try: import comtypes self.comtypes = comtypes @@ -298,16 +292,16 @@ def _load_comtypes(self): except ImportError: return False - return True # notifiy comtypes was loaded + return True # 通知 comtypes 已加载 def __init__(self): - self._connected = False # modules/objects created + self._connected = False # module/object 是否已创建 - self.notifs = collections.deque() # hold notifications to deliver + self.notifs = collections.deque() # 保存待发送通知 - self.t_vcconn = None # control connection status + self.t_vcconn = None # 控制连接状态 - # hold deques to market data symbols + # 保存 market data symbol 对应的队列 self._dqs = collections.deque() self._qdatas = dict() self._tftable = dict() @@ -319,7 +313,7 @@ def __init__(self): return vctypelibs = self.find_vchart() - # Try to load the modules + # 尝试加载模块 try: self.vcdsmod = self.GetModule(vctypelibs[0]) self.vcrtmod = self.GetModule(vctypelibs[1]) @@ -333,7 +327,7 @@ def __init__(self): self.put_notification(msg, *msg) return - # Try to load the main objects + # 尝试加载主对象 try: self.vcds = self.CreateObject(self.vcdsmod.DataSourceManager) # self.vcrt = self.CreateObject(self.vcrtmod.RealTime) @@ -352,13 +346,13 @@ def __init__(self): self._connected = True - # Build a table of VCRT Field_XX mappings for debugging purposes + # 为调试目的构建 VCRT Field_XX 映射表 self.vcrtfields = dict() for name in dir(self.vcrtmod): if name.startswith('Field'): self.vcrtfields[getattr(self.vcrtmod, name)] = name - # Modules and objects can be created + # module 和 object 已可创建 self._tftable = { TimeFrame.Ticks: (self.vcdsmod.CT_Ticks, 1), TimeFrame.MicroSeconds: (self.vcdsmod.CT_Ticks, 1), # To Resample @@ -374,18 +368,18 @@ def put_notification(self, msg, *args, **kwargs): self.notifs.append((msg, args, kwargs)) def get_notifications(self): - '''Return the pending "store" notifications''' - self.notifs.append(None) # Mark current end of notifs - return [x for x in iter(self.notifs.popleft, None)] # popleft til None + '''返回待处理的 "store" 通知。''' + self.notifs.append(None) # 标记当前通知结尾 + return [x for x in iter(self.notifs.popleft, None)] # popleft 直到 None def start(self, data=None, broker=None): if not self._connected: return if self.t_vcconn is None: - # Kickstart connection thread check + # 启动连接状态检查线程 self.t_vcconn = t = threading.Thread(target=self._start_vcrt) - t.daemon = True # Do not stop a general exit + t.daemon = True # 不阻止整体退出 t.start() if broker is not None: @@ -394,14 +388,14 @@ def start(self, data=None, broker=None): t.start() def stop(self): - pass # nothing to do + pass # 无需操作 def connected(self): return self._connected def _start_vcrt(self): - # Use VCRealTime to monitor the connection status - self.comtypes.CoInitialize() # running in another thread + # 使用 VCRealTime 监控连接状态 + self.comtypes.CoInitialize() # 在另一个线程中运行 vcrt = self.CreateObject(self.vcrtmod.RealTime) sink = RTEventSink(self) conn = self.GetEvents(vcrt, sink) @@ -411,7 +405,7 @@ def _start_vcrt(self): def _vcrt_connection(self, status): if status == -0xffff: txt = 'VisualChart shutting down', - # p2: 0 -> Disconnected / p2: 1 -> Reconnected + # p2: 0 -> Disconnected / p2: 1 -> Reconnected elif status == -0xfff0: txt = 'VisualChart is Disconnected' elif status == -0xfff1: @@ -426,12 +420,12 @@ def _vcrt_connection(self, status): q.put(status) def _tf2ct(self, timeframe, compression): - # Translates timeframes to known compression types in VisualChart + # 将 timeframe 转换为 VisualChart 已知 compression type timeframe, extracomp = self._tftable[timeframe] return timeframe, compression * extracomp def _ticking(self, timeframe): - # Translates timeframes to known compression types in VisualChart + # 将 timeframe 转换为 VisualChart 已知 compression type vctimeframe, _ = self._tftable[timeframe] return vctimeframe == self.vcdsmod.CT_Ticks @@ -451,20 +445,20 @@ def _rtdata(self, data, symbol): t.daemon = True t.start() - # Broker functions + # Broker 函数 def _t_rtdata(self, data, symbol): - self.comtypes.CoInitialize() # running in another thread + self.comtypes.CoInitialize() # 在另一个线程中运行 vcrt = self.CreateObject(self.vcrtmod.RealTime) conn = self.GetEvents(vcrt, data) data._vcrt = vcrt - vcrt.RequestSymbolFeed(symbol, False) # no limits + vcrt.RequestSymbolFeed(symbol, False) # 不设限制 PumpEvents() - del conn # ensure events go away + del conn # 确保事件连接释放 self.comtypes.CoUninitialize() def _symboldata(self, symbol): - # Assumption -> we are connected and the symbol has been found + # 假设已连接且 symbol 已找到 self.vcds.ActiveEvents = 0 # self.vcds.EventsType = self.vcdsmod.EF_Always @@ -483,9 +477,9 @@ def _directdata(self, data, symbol, timeframe, compression, d1, d2=None, historical=False): - # Assume the data has checked the existence of the symbol + # 假设 data 已经检查 symbol 存在 timeframe, compression = self._tf2ct(timeframe, compression) - kwargs = locals().copy() # make a copy of the args + kwargs = locals().copy() # 复制参数 kwargs.pop('self') kwargs['q'] = q = self._getq(data) @@ -493,14 +487,14 @@ def _directdata(self, data, t.daemon = True t.start() - # use the queue to synchronize until symbolinfo has been gotten - return q # tell the caller where to expect the hist data + # 使用 queue 同步,直到 symbolinfo 已获取 + return q # 告诉调用方从哪里接收历史数据 def _t_directdata(self, data, symbol, timeframe, compression, d1, d2, q, historical): - self.comtypes.CoInitialize() # start com threading + self.comtypes.CoInitialize() # 启动 COM 线程 vcds = self.CreateObject(self.vcdsmod.DataSourceManager) historical = historical or d2 is not None @@ -517,29 +511,29 @@ def _t_directdata(self, data, data._setserie(serie) - # processing of bars can continue + # bar 处理可以继续 data.OnNewDataSerieBar(serie, forcepush=historical) if historical: # push the last bar - q.put(None) # Signal end of transmission + q.put(None) # 标记传输结束 dsconn = None else: - dsconn = self.GetEvents(vcds, data) # finally connect the events + dsconn = self.GetEvents(vcds, data) # 最后连接事件 pass - # pump events in this thread - call ping + # 在该线程中 pump events,并调用 ping PumpEvents(timeout=data._getpingtmout, cb=data.ping) if dsconn is not None: - del dsconn # Docs recommend deleting the connection + del dsconn # 文档建议删除连接 - # Delete the series before coming out of the thread + # 退出线程前删除 series vcds.DeleteDataSource(serie) - self.comtypes.CoUninitialize() # Terminate com threading + self.comtypes.CoUninitialize() # 终止 COM 线程 - # Broker functions + # Broker 函数 def _t_broker(self, broker): - self.comtypes.CoInitialize() # running in another thread + self.comtypes.CoInitialize() # 在另一个线程中运行 trader = self.CreateObject(self.vcctmod.Trader) conn = self.GetEvents(trader, broker(trader)) PumpEvents() - del conn # ensure events go away + del conn # 确保事件连接释放 self.comtypes.CoUninitialize() diff --git a/backtrader/strategies/sma_crossover.py b/backtrader/strategies/sma_crossover.py index 3001ee886..434b01b3a 100644 --- a/backtrader/strategies/sma_crossover.py +++ b/backtrader/strategies/sma_crossover.py @@ -27,35 +27,36 @@ class MA_CrossOver(bt.Strategy): - '''This is a long-only strategy which operates on a moving average cross + '''基于 moving average cross 的 long-only strategy。 - Note: - - Although the default + 当 fast moving average 向上穿越 slow moving average 且当前没有 position 时买入; + 当 fast moving average 向下穿越 slow moving average 且当前已有 position 时卖出。 - Buy Logic: - - No position is open on the data + Args: + fast: fast moving average 的 period。 + slow: slow moving average 的 period。 + _movav: 使用的 moving average 类,默认 ``btind.MovAv.SMA``。 - - The ``fast`` moving averagecrosses over the ``slow`` strategy to the - upside. + Returns: + MA_CrossOver: 使用 ``Market`` order 执行均线交叉信号的 strategy。 - Sell Logic: - - A position exists on the data - - - The ``fast`` moving average crosses over the ``slow`` strategy to the - downside - - Order Execution Type: - - Market + --- + 交互示例: + >>> import backtrader as bt + >>> from backtrader.strategies import MA_CrossOver + >>> cerebro = bt.Cerebro() + >>> cerebro.addstrategy(MA_CrossOver, fast=5, slow=20) + 0 ''' alias = ('SMA_CrossOver',) params = ( - # period for the fast Moving Average + # fast Moving Average 的 period ('fast', 10), - # period for the slow moving average + # slow Moving Average 的 period ('slow', 30), - # moving average to use + # 使用的 moving average ('_movav', btind.MovAv.SMA) ) diff --git a/backtrader/strategy.py b/backtrader/strategy.py index e5115688d..93a28d61d 100644 --- a/backtrader/strategy.py +++ b/backtrader/strategy.py @@ -44,21 +44,19 @@ class MetaStrategy(StrategyBase.__class__): _indcol = dict() def __new__(meta, name, bases, dct): - # Hack to support original method name for notify_order + # 兼容 notify_order 的旧方法名 if 'notify' in dct: - # rename 'notify' to 'notify_order' + # 将 'notify' 重命名为 'notify_order' dct['notify_order'] = dct.pop('notify') if 'notify_operation' in dct: - # rename 'notify' to 'notify_order' + # 将 'notify_operation' 重命名为 'notify_trade' dct['notify_trade'] = dct.pop('notify_operation') return super(MetaStrategy, meta).__new__(meta, name, bases, dct) def __init__(cls, name, bases, dct): - ''' - Class has already been created ... register subclasses - ''' - # Initialize the class + '''类已经创建完成,注册其 subclasses。''' + # 初始化 class super(MetaStrategy, cls).__init__(name, bases, dct) if not cls.aliased and \ @@ -68,7 +66,7 @@ def __init__(cls, name, bases, dct): def donew(cls, *args, **kwargs): _obj, args, kwargs = super(MetaStrategy, cls).donew(*args, **kwargs) - # Find the owner and store it + # 查找 owner 并保存 _obj.env = _obj.cerebro = cerebro = findowner(_obj, bt.Cerebro) _obj._id = cerebro._next_stid() @@ -105,35 +103,38 @@ def dopostinit(cls, _obj, *args, **kwargs): class Strategy(with_metaclass(MetaStrategy, StrategyBase)): - ''' - Base class to be subclassed for user defined strategies. - ''' + '''用户自定义 strategies 的基类,用于承载交易逻辑和生命周期回调。''' _ltype = LineIterator.StratType csv = True - _oldsync = False # update clock using old methodology : data 0 + _oldsync = False # 使用旧方法更新 clock:以 data 0 为准 - # keep the latest delivered data date in the line + # 在 line 中保存最新交付的 data 日期 lines = ('datetime',) def qbuffer(self, savemem=0, replaying=False): - '''Enable the memory saving schemes. Possible values for ``savemem``: + '''启用 memory saving schemes。 + + Args: + - ``savemem``: 内存节省级别。 + + ``0`` 表示不节省内存,每个 lines object 在内存中保存全部值。 - 0: No savings. Each lines object keeps in memory all values + ``1`` 表示所有 lines objects 都节省内存,仅保留严格所需的最小数据。 - 1: All lines objects save memory, using the strictly minimum needed + 负值用于仍然需要 plotting 的场景: - Negative values are meant to be used when plotting is required: + ``-1`` 表示 Strategy 层级的 Indicators 和 Observers 不启用 memory + saving,但它们下方声明的对象会启用。 - -1: Indicators at Strategy Level and Observers do not enable memory - savings (but anything declared below it does) + ``-2`` 在 ``-1`` 基础上,还会为声明了 *plotinfo.plot* 为 ``False`` + 的 indicators 启用 memory saving(这些对象不会被绘图)。 - -2: Same as -1 plus activation of memory saving for any indicators - which has declared *plotinfo.plot* as False (will not be plotted) + - ``replaying``: 是否处于 replaying 模式。 ''' if savemem < 0: - # Get any attribute which labels itself as Indicator + # 获取所有标记为 Indicator 的属性 for ind in self._lineiterators[self.IndType]: subsave = isinstance(ind, (LineSingle,)) if not subsave and savemem < -1: @@ -147,7 +148,7 @@ def qbuffer(self, savemem=0, replaying=False): for line in self.lines: line.qbuffer(savemem=1) - # Save in all object types depending on the strategy + # 对依附于 strategy 的所有对象类型启用节省 for itcls in self._lineiterators: for it in self._lineiterators[itcls]: it.qbuffer(savemem=1) @@ -157,8 +158,8 @@ def _periodset(self): _dminperiods = collections.defaultdict(list) for lineiter in self._lineiterators[LineIterator.IndType]: - # if multiple datas are used and multiple timeframes the larger - # timeframe may place larger time constraints in calling next. + # 如果使用多个 datas 且 timeframe 不同,较大的 timeframe + # 可能会对 next 调用施加更大的时间约束 clk = getattr(lineiter, '_clock', None) if clk is None: clk = getattr(lineiter._owner, '_clock', None) @@ -169,21 +170,20 @@ def _periodset(self): if id(clk) in dataids: break # already top-level clock (data feed) - # See if the current clock has higher level clocks + # 检查当前 clock 是否有更高层级的 clocks clk2 = getattr(clk, '_clock', None) if clk2 is None: clk2 = getattr(clk._owner, '_clock', None) if clk2 is None: - break # if no clock found, bail out + break # 如果找不到 clock,退出 - clk = clk2 # keep the ref and try to go up the hierarchy + clk = clk2 # 保留引用并尝试沿层级向上查找 if clk is None: - continue # no clock found, go to next + continue # 找不到 clock,进入下一个 - # LineSeriesStup wraps a line and the clock is the wrapped line and - # no the wrapper itself. + # LineSeriesStub 包装一条 line,clock 是被包装的 line,而不是 wrapper 自身 if isinstance(clk, LineSeriesStub): clk = clk.lines[0] @@ -192,33 +192,32 @@ def _periodset(self): self._minperiods = list() for data in self.datas: - # Do not only consider the data as clock but also its lines which - # may have been individually passed as clock references and - # discovered as clocks above + # 不仅把 data 作为 clock,也考虑其 lines;这些 lines 可能被单独作为 + # clock 引用传入,并在上方发现 - # Initialize with data min period if any + # 如果存在 data min period,则用它初始化 dlminperiods = _dminperiods[data] - for l in data.lines: # search each line for min periods + for l in data.lines: # 在每条 line 中搜索 min periods if l in _dminperiods: - dlminperiods += _dminperiods[l] # found, add it + dlminperiods += _dminperiods[l] # 找到则加入 - # keep the reference to the line if any was found + # 如果找到任何引用,则保留到 line 的引用 _dminperiods[data] = [max(dlminperiods)] if dlminperiods else [] dminperiod = max(_dminperiods[data] or [data._minperiod]) self._minperiods.append(dminperiod) - # Set the minperiod + # 设置 minperiod minperiods = \ [x._minperiod for x in self._lineiterators[LineIterator.IndType]] self._minperiod = max(minperiods or [self._minperiod]) def _addwriter(self, writer): - ''' - Unlike the other _addxxx functions this one receives an instance - because the writer works at cerebro level and is only passed to the - strategy to simplify the logic + '''添加 writer 实例。 + + 与其他 ``_addxxx`` 函数不同,这里接收实例,因为 writer 工作在 Cerebro + 层级,只是传给 strategy 以简化逻辑。 ''' self.writers.append(writer) @@ -226,12 +225,12 @@ def _addindicator(self, indcls, *indargs, **indkwargs): indcls(*indargs, **indkwargs) def _addanalyzer_slave(self, ancls, *anargs, **ankwargs): - '''Like _addanalyzer but meant for observers (or other entities) which - rely on the output of an analyzer for the data. These analyzers have - not been added by the user and are kept separate from the main - analyzers + '''类似 ``_addanalyzer``,但用于 observers 或其他依赖 analyzer 输出的实体。 + + 这些 analyzers 不是由用户添加,会与主 analyzers 分开保存。 - Returns the created analyzer + Returns: + Analyzer: 创建出的 analyzer。 ''' analyzer = ancls(*anargs, **ankwargs) self._slave_analyzers.append(analyzer) @@ -243,7 +242,7 @@ def _getanalyzer_slave(self, idx): def _addanalyzer(self, ancls, *anargs, **ankwargs): anname = ankwargs.pop('_name', '') or ancls.__name__.lower() nsuffix = next(self._alnames[anname]) - anname += str(nsuffix or '') # 0 (first instance) gets no suffix + anname += str(nsuffix or '') # 0(首个实例)不加 suffix analyzer = ancls(*anargs, **ankwargs) self.analyzers.append(analyzer, anname) @@ -266,7 +265,7 @@ def _addobserver(self, multi, obscls, *obsargs, **obskwargs): l.append(obs) def _getminperstatus(self): - # check the min period status connected to datas + # 检查与 datas 相关的 min period 状态 dlens = map(operator.sub, self._minperiods, map(len, self.datas)) self._minperstatus = minperstatus = max(dlens) return minperstatus @@ -285,7 +284,7 @@ def _oncepost_open(self): if minperstatus < 0: self.next_open() elif minperstatus == 0: - self.nextstart_open() # only called for the 1st value + self.nextstart_open() # 仅针对第 1 个值调用 else: self.prenext_open() @@ -295,10 +294,10 @@ def _oncepost(self, dt): indicator.advance() if self._oldsync: - # Strategy has not been reset, the line is there + # Strategy 尚未 reset,line 仍在当前位置 self.advance() else: - # strategy has been reset to beginning. advance step by step + # strategy 已 reset 到开头,需要逐步 forward self.forward() self.lines.datetime[0] = dt @@ -308,7 +307,7 @@ def _oncepost(self, dt): if minperstatus < 0: self.next() elif minperstatus == 0: - self.nextstart() # only called for the 1st value + self.nextstart() # 仅针对第 1 个值调用 else: self.prenext() @@ -339,7 +338,7 @@ def _next_open(self): if minperstatus < 0: self.next_open() elif minperstatus == 0: - self.nextstart_open() # only called for the 1st value + self.nextstart_open() # 仅针对第 1 个值调用 else: self.prenext_open() @@ -358,7 +357,7 @@ def _next_observers(self, minperstatus, once=False): if minperstatus < 0: analyzer._next() elif minperstatus == 0: - analyzer._nextstart() # only called for the 1st value + analyzer._nextstart() # 仅针对第 1 个值调用 else: analyzer._prenext() @@ -372,7 +371,7 @@ def _next_observers(self, minperstatus, once=False): if minperstatus < 0: observer.next() elif minperstatus == 0: - observer.nextstart() # only called for the 1st value + observer.nextstart() # 仅针对第 1 个值调用 elif len(observer): observer.prenext() else: @@ -383,7 +382,7 @@ def _next_analyzers(self, minperstatus, once=False): if minperstatus < 0: analyzer._next() elif minperstatus == 0: - analyzer._nextstart() # only called for the 1st value + analyzer._nextstart() # 仅针对第 1 个值调用 else: analyzer._prenext() @@ -398,22 +397,22 @@ def _start(self): for obs in self.observers: if not isinstance(obs, list): - obs = [obs] # support of multi-data observers + obs = [obs] # 支持 multi-data observers for o in obs: o._start() - # change operators to stage 2 + # 将 operators 切换到 stage 2 self._stage2() self._dlens = [len(data) for data in self.datas] - self._minperstatus = MAXINT # start in prenext + self._minperstatus = MAXINT # 从 prenext 开始 self.start() def start(self): - '''Called right before the backtesting is about to be started.''' + '''在 backtesting 即将开始前调用。''' pass def getwriterheaders(self): @@ -425,7 +424,7 @@ def getwriterheaders(self): headers = list() - # prepare the indicators/observers data headers + # 准备 indicators/observers 的 data headers for iocsv in self.indobscsv: name = iocsv.plotinfo.plotname or iocsv.__class__.__name__ headers.append(name) @@ -468,11 +467,11 @@ def getwriterinfo(self): ainfo = wrinfo.Analyzers - # Internal Value Analyzer + # 内部 Value Analyzer ainfo.Value.Begin = self.broker.startingcash ainfo.Value.End = self.broker.getvalue() - # no slave analyzers for writer + # writer 不输出 slave analyzers for aname, analyzer in self.analyzers.getitems(): ainfo[aname].Params = analyzer.p._getkwargs() or None ainfo[aname].Analysis = analyzer.get_analysis() @@ -485,11 +484,11 @@ def _stop(self): for analyzer in itertools.chain(self.analyzers, self._slave_analyzers): analyzer._stop() - # change operators back to stage 1 - allows reuse of datas + # 将 operators 切回 stage 1,以允许复用 datas self._stage1() def stop(self): - '''Called right before the backtesting is about to be stopped''' + '''在 backtesting 即将停止前调用。''' pass def set_tradehistory(self, onoff=True): @@ -543,7 +542,7 @@ def _addnotification(self, order, quicknotify=False): if quicknotify: qtrades.append(copy.copy(trade)) - # Update it if needed + # 按需更新 if exbit.opened: if trade.isclosed: trade = Trade(data=tradedata, tradeid=order.tradeid, @@ -558,9 +557,8 @@ def _addnotification(self, order, quicknotify=False): exbit.pnl, comminfo=order.comminfo) - # This extra check covers the case in which different tradeid - # orders have put the position down to 0 and the next order - # "opens" a position but "closes" the trade + # 这个额外检查覆盖如下场景:不同 tradeid 的 orders 将 position 降到 0, + # 而下一个 order “打开” position 的同时又“关闭” trade if trade.isclosed: self._tradespending.append(copy.copy(trade)) if quicknotify: @@ -576,9 +574,8 @@ def _addnotification(self, order, quicknotify=False): def _notify(self, qorders=[], qtrades=[]): if self.cerebro.p.quicknotify: - # need to know if quicknotify is on, to not reprocess pendingorders - # and pendingtrades, which have to exist for things like observers - # which look into it + # 需要知道 quicknotify 是否开启,以避免重复处理 pendingorders 和 pendingtrades; + # 它们必须存在,因为 observers 等对象可能会查看它们 procorders = qorders proctrades = qtrades else: @@ -599,7 +596,7 @@ def _notify(self, qorders=[], qtrades=[]): analyzer._notify_trade(trade) if qorders: - return # cash is notified on a regular basis + return # cash 会按常规节奏通知 cash = self.broker.getcash() value = self.broker.getvalue() @@ -619,90 +616,58 @@ def add_timer(self, when, allow=None, tzdata=None, cheat=False, *args, **kwargs): - ''' - **Note**: can be called during ``__init__`` or ``start`` - - Schedules a timer to invoke either a specified callback or the - ``notify_timer`` of one or more strategies. - - Arguments: - - - ``when``: can be - - - ``datetime.time`` instance (see below ``tzdata``) - - ``bt.timer.SESSION_START`` to reference a session start - - ``bt.timer.SESSION_END`` to reference a session end - - - ``offset`` which must be a ``datetime.timedelta`` instance - - Used to offset the value ``when``. It has a meaningful use in - combination with ``SESSION_START`` and ``SESSION_END``, to indicated - things like a timer being called ``15 minutes`` after the session - start. - - - ``repeat`` which must be a ``datetime.timedelta`` instance - - Indicates if after a 1st call, further calls will be scheduled - within the same session at the scheduled ``repeat`` delta - - Once the timer goes over the end of the session it is reset to the - original value for ``when`` - - - ``weekdays``: a **sorted** iterable with integers indicating on - which days (iso codes, Monday is 1, Sunday is 7) the timers can - be actually invoked + '''添加 strategy 级 timer。 - If not specified, the timer will be active on all days + **注意**:可在 ``__init__`` 或 ``start`` 中调用。 - - ``weekcarry`` (default: ``False``). If ``True`` and the weekday was - not seen (ex: trading holiday), the timer will be executed on the - next day (even if in a new week) + 该方法会安排 timer,在触发时调用当前 strategy 的 ``notify_timer``。 - - ``monthdays``: a **sorted** iterable with integers indicating on - which days of the month a timer has to be executed. For example - always on day *15* of the month + Args: + - ``when``: timer 的触发时间,可以是: - If not specified, the timer will be active on all days + - ``datetime.time`` 实例(见下方 ``tzdata``)。 + - ``bt.timer.SESSION_START``,表示 session 开始。 + - ``bt.timer.SESSION_END``,表示 session 结束。 - - ``monthcarry`` (default: ``True``). If the day was not seen - (weekend, trading holiday), the timer will be executed on the next - available day. + - ``offset``: ``datetime.timedelta`` 实例,用于偏移 ``when``。与 + ``SESSION_START`` / ``SESSION_END`` 配合时尤其有意义,例如 session + 开始后 ``15 minutes`` 触发。 - - ``allow`` (default: ``None``). A callback which receives a - `datetime.date`` instance and returns ``True`` if the date is - allowed for timers or else returns ``False`` + - ``repeat``: ``datetime.timedelta`` 实例。第 1 次触发后,是否在同一 + session 内按该间隔继续调度。超过 session 结束后,会重置为原始 ``when``。 - - ``tzdata`` which can be either ``None`` (default), a ``pytz`` - instance or a ``data feed`` instance. + - ``weekdays``: **已排序** 的整数 iterable,表示 timer 可实际触发的星期; + ISO code 中 Monday 为 1,Sunday 为 7。未指定时,对所有天生效。 - ``None``: ``when`` is interpreted at face value (which translates - to handling it as if it where UTC even if it's not) + - ``weekcarry``: 如果为 ``True``,且指定 weekday 未出现(例如交易假日), + timer 会在下一天执行,即便进入新的一周。 - ``pytz`` instance: ``when`` will be interpreted as being specified - in the local time specified by the timezone instance. + - ``monthdays``: **已排序** 的整数 iterable,表示每月哪些日期触发 timer, + 例如每月 *15* 日。未指定时,对所有天生效。 - ``data feed`` instance: ``when`` will be interpreted as being - specified in the local time specified by the ``tz`` parameter of - the data feed instance. + - ``monthcarry``: 如果指定日期未出现(周末、交易假日),timer 会在下一个 + 可用日期执行。 - **Note**: If ``when`` is either ``SESSION_START`` or - ``SESSION_END`` and ``tzdata`` is ``None``, the 1st *data feed* - in the system (aka ``self.data0``) will be used as the reference - to find out the session times. + - ``allow``: 可选 callback,接收 ``datetime.date`` 实例,并返回该日期是否 + 允许触发 timer。 - - ``cheat`` (default ``False``) if ``True`` the timer will be called - before the broker has a chance to evaluate the orders. This opens - the chance to issue orders based on opening price for example right - before the session starts + - ``tzdata``: 可以为 ``None``、``pytz`` 实例或 ``data feed`` 实例。 + ``None`` 表示按字面解释 ``when``(等同于按 UTC 处理)。传入 ``pytz`` 时, + ``when`` 按该 timezone 的本地时间解释;传入 ``data feed`` 时,按该 data + feed 的 ``tz`` 参数解释。 - - ``*args``: any extra args will be passed to ``notify_timer`` + **注意**:如果 ``when`` 是 ``SESSION_START`` 或 ``SESSION_END`` 且 + ``tzdata`` 为 ``None``,系统会使用第 1 个 *data feed*(即 + ``self.data0``)作为 session 时间参考。 - - ``**kwargs``: any extra kwargs will be passed to ``notify_timer`` + - ``cheat``: 如果为 ``True``,timer 会在 broker 有机会评估 orders 前触发。 + 这允许在 session 开始前基于 opening price 之类的信息发出 orders。 - Return Value: - - - The created timer + - ``*args``: 额外位置参数,会传给 ``notify_timer``。 + - ``**kwargs``: 额外关键字参数,会传给 ``notify_timer``。 + Returns: + Timer: 创建出的 timer。 ''' return self.cerebro._add_timer( owner=self, when=when, offset=offset, repeat=repeat, @@ -713,62 +678,51 @@ def add_timer(self, when, *args, **kwargs) def notify_timer(self, timer, when, *args, **kwargs): - '''Receives a timer notification where ``timer`` is the timer which was - returned by ``add_timer``, and ``when`` is the calling time. ``args`` - and ``kwargs`` are any additional arguments passed to ``add_timer`` - - The actual ``when`` time can be later, but the system may have not be - able to call the timer before. This value is the timer value and no the - system time. + '''接收 timer notification。 + + Args: + - ``timer``: ``add_timer`` 返回的 timer。 + - ``when``: timer 计划触发时间。实际调用时间可能更晚;该值表示 timer time, + 不是系统当前时间。 + - ``*args``: ``add_timer`` 传入的额外位置参数。 + - ``**kwargs``: ``add_timer`` 传入的额外关键字参数。 ''' pass def notify_cashvalue(self, cash, value): - ''' - Receives the current fund value, value status of the strategy's broker - ''' + '''接收 strategy broker 的当前 cash 和 value 状态。''' pass def notify_fund(self, cash, value, fundvalue, shares): - ''' - Receives the current cash, value, fundvalue and fund shares - ''' + '''接收当前 cash、value、fundvalue 和 fund shares。''' pass def notify_order(self, order): - ''' - Receives an order whenever there has been a change in one - ''' + '''当 order 状态变化时接收该 order。''' pass def notify_trade(self, trade): - ''' - Receives a trade whenever there has been a change in one - ''' + '''当 trade 状态变化时接收该 trade。''' pass def notify_store(self, msg, *args, **kwargs): - '''Receives a notification from a store provider''' + '''接收来自 store provider 的 notification。''' pass def notify_data(self, data, status, *args, **kwargs): - '''Receives a notification from data''' + '''接收来自 data 的 notification。''' pass def getdatanames(self): - ''' - Returns a list of the existing data names - ''' + '''返回现有 data names 列表。''' return keys(self.env.datasbyname) def getdatabyname(self, name): - ''' - Returns a given data by name using the environment (cerebro) - ''' + '''通过环境(cerebro)按名称返回指定 data。''' return self.env.datasbyname[name] def cancel(self, order): - '''Cancels the order in the broker''' + '''在 broker 中取消 order。''' self.broker.cancel(order) def buy(self, data=None, @@ -777,150 +731,68 @@ def buy(self, data=None, trailamount=None, trailpercent=None, parent=None, transmit=True, **kwargs): - '''Create a buy (long) order and send it to the broker - - - ``data`` (default: ``None``) - - For which data the order has to be created. If ``None`` then the - first data in the system, ``self.datas[0] or self.data0`` (aka - ``self.data``) will be used - - - ``size`` (default: ``None``) - - Size to use (positive) of units of data to use for the order. - - If ``None`` the ``sizer`` instance retrieved via ``getsizer`` will - be used to determine the size. + '''创建 buy/long order,并发送给 broker。 - - ``price`` (default: ``None``) + Args: + - ``data``: order 所属 data。为 ``None`` 时使用系统第 1 个 data,即 + ``self.datas[0]`` / ``self.data0`` / ``self.data``。 - Price to use (live brokers may place restrictions on the actual - format if it does not comply to minimum tick size requirements) + - ``size``: 正数 data units 数量。为 ``None`` 时,通过 ``getsizer`` 取得的 + ``sizer`` 实例自动计算。 - ``None`` is valid for ``Market`` and ``Close`` orders (the market - determines the price) + - ``price``: order 使用的价格。live brokers 可能会因为 minimum tick size + 等要求限制格式。``Market`` 和 ``Close`` orders 可使用 ``None``,价格由市场 + 决定;对 ``Limit``、``Stop`` 和 ``StopLimit``,该值表示触发点或成交价格。 - For ``Limit``, ``Stop`` and ``StopLimit`` orders this value - determines the trigger point (in the case of ``Limit`` the trigger - is obviously at which price the order should be matched) + - ``plimit``: 仅适用于 ``StopLimit`` orders。``Stop`` 被触发后,用该价格 + 设置隐含的 *Limit* order。 - - ``plimit`` (default: ``None``) + - ``trailamount``: ``StopTrail`` / ``StopTrailLimit`` 使用的绝对 trailing + stop 距离。 - Only applicable to ``StopLimit`` orders. This is the price at which - to set the implicit *Limit* order, once the *Stop* has been - triggered (for which ``price`` has been used) + - ``trailpercent``: ``StopTrail`` / ``StopTrailLimit`` 使用的百分比 trailing + stop 距离;如果也指定了 ``trailamount``,优先使用 ``trailamount``。 - - ``trailamount`` (default: ``None``) + - ``exectype``: execution type。可选值包括: - If the order type is StopTrail or StopTrailLimit, this is an - absolute amount which determines the distance to the price (below - for a Sell order and above for a buy order) to keep the trailing - stop + - ``Order.Market`` 或 ``None``: 下一可用价格执行;backtesting 中通常是 + 下一根 bar 的 opening price。 + - ``Order.Limit``: 仅在给定 ``price`` 或更优价格执行。 + - ``Order.Stop``: 到达 ``price`` 后触发,并像 ``Market`` order 一样执行。 + - ``Order.StopLimit``: 到达 ``price`` 后触发,并以 ``plimit`` 创建隐含 + *Limit* order。 + - ``Order.Close``: 仅以 session closing price 执行,通常发生在 closing + auction。 + - ``Order.StopTrail``: 按 ``price`` 减去 ``trailamount`` 或 + ``trailpercent`` 触发,并随价格远离 stop 更新。 + - ``Order.StopTrailLimit``: 类似 ``StopTrail``,但触发后使用 limit 逻辑。 - - ``trailpercent`` (default: ``None``) + - ``valid``: order 有效期。``None`` 表示 *Good till cancel*;也可以传入 + ``datetime.datetime`` / ``datetime.date`` 作为 *good till date*; + ``Order.DAY``、``0`` 或 ``timedelta()`` 表示当日有效到 session 结束; + numeric value 会按 ``backtrader`` 使用的 matplotlib datetime 编码解释。 - If the order type is StopTrail or StopTrailLimit, this is a - percentage amount which determines the distance to the price (below - for a Sell order and above for a buy order) to keep the trailing - stop (if ``trailamount`` is also specified it will be used) + - ``tradeid``: backtrader 内部用于跟踪同一 asset 上重叠 trades 的 id; + order 状态通知会把它传回 strategy。 - - ``exectype`` (default: ``None``) + - ``oco``: 另一个 order 实例。当前 order 会加入 OCO(Order Cancel Others) + 组;组内任一 order 执行后,会立即取消其他 orders。 - Possible values: + - ``parent``: order 组的父子关系。例如 bracket order 中,父 buy order + 可被 high-side limit sell 和 low-side stop sell 包围;子 orders 在父 order + 执行前保持 inactive,父 order 取消/过期时子 orders 也会取消。 - - ``Order.Market`` or ``None``. A market order will be executed - with the next available price. In backtesting it will be the - opening price of the next bar + - ``transmit``: 是否将 order **transmitted** 给 broker。它可用于控制 + bracket orders,例如先放置父 order 和首批 children,最后一个 child 再触发 + 整组 bracket orders 的提交。 - - ``Order.Limit``. An order which can only be executed at the given - ``price`` or better - - - ``Order.Stop``. An order which is triggered at ``price`` and - executed like an ``Order.Market`` order - - - ``Order.StopLimit``. An order which is triggered at ``price`` and - executed as an implicit *Limit* order with price given by - ``pricelimit`` - - - ``Order.Close``. An order which can only be executed with the - closing price of the session (usually during a closing auction) - - - ``Order.StopTrail``. An order which is triggered at ``price`` - minus ``trailamount`` (or ``trailpercent``) and which is updated - if the price moves away from the stop - - - ``Order.StopTrailLimit``. An order which is triggered at - ``price`` minus ``trailamount`` (or ``trailpercent``) and which - is updated if the price moves away from the stop - - - ``valid`` (default: ``None``) - - Possible values: - - - ``None``: this generates an order that will not expire (aka - *Good till cancel*) and remain in the market until matched or - canceled. In reality brokers tend to impose a temporal limit, - but this is usually so far away in time to consider it as not - expiring - - - ``datetime.datetime`` or ``datetime.date`` instance: the date - will be used to generate an order valid until the given - datetime (aka *good till date*) - - - ``Order.DAY`` or ``0`` or ``timedelta()``: a day valid until - the *End of the Session* (aka *day* order) will be generated - - - ``numeric value``: This is assumed to be a value corresponding - to a datetime in ``matplotlib`` coding (the one used by - ``backtrader``) and will used to generate an order valid until - that time (*good till date*) - - - ``tradeid`` (default: ``0``) - - This is an internal value applied by ``backtrader`` to keep track - of overlapping trades on the same asset. This ``tradeid`` is sent - back to the *strategy* when notifying changes to the status of the - orders. - - - ``oco`` (default: ``None``) - - Another ``order`` instance. This order will become part of an OCO - (Order Cancel Others) group. The execution of one of the orders, - immediately cancels all others in the same group - - - ``parent`` (default: ``None``) - - Controls the relationship of a group of orders, for example a buy - which is bracketed by a high-side limit sell and a low side stop - sell. The high/low side orders remain inactive until the parent - order has been either executed (they become active) or is - canceled/expires (the children are also canceled) bracket orders - have the same size - - - ``transmit`` (default: ``True``) - - Indicates if the order has to be **transmitted**, ie: not only - placed in the broker but also issued. This is meant for example to - control bracket orders, in which one disables the transmission for - the parent and 1st set of children and activates it for the last - children, which triggers the full placement of all bracket orders. - - - ``**kwargs``: additional broker implementations may support extra - parameters. ``backtrader`` will pass the *kwargs* down to the - created order objects - - Example: if the 4 order execution types directly supported by - ``backtrader`` are not enough, in the case of for example - *Interactive Brokers* the following could be passed as *kwargs*:: - - orderType='LIT', lmtPrice=10.0, auxPrice=9.8 - - This would override the settings created by ``backtrader`` and - generate a ``LIMIT IF TOUCHED`` order with a *touched* price of 9.8 - and a *limit* price of 10.0. + - ``**kwargs``: 额外 broker 参数。backtrader 会把它们传给创建出的 order + 对象。比如 Interactive Brokers 可通过 ``orderType='LIT'``、 + ``lmtPrice=10.0``、``auxPrice=9.8`` 覆盖默认设置,生成 + ``LIMIT IF TOUCHED`` order。 Returns: - - the submitted order + Order | None: 提交后的 order;如果最终 size 为 0,则返回 ``None``。 ''' if isinstance(data, string_types): @@ -946,12 +818,12 @@ def sell(self, data=None, trailamount=None, trailpercent=None, parent=None, transmit=True, **kwargs): - ''' - To create a selll (short) order and send it to the broker + '''创建 sell/short order,并发送给 broker。 - See the documentation for ``buy`` for an explanation of the parameters + 参数含义见 ``buy`` 的说明。 - Returns: the submitted order + Returns: + Order | None: 提交后的 order;如果最终 size 为 0,则返回 ``None``。 ''' if isinstance(data, string_types): data = self.getdatabyname(data) @@ -971,17 +843,17 @@ def sell(self, data=None, return None def close(self, data=None, size=None, **kwargs): - ''' - Counters a long/short position closing it - - See the documentation for ``buy`` for an explanation of the parameters + '''反向下单以关闭 long/short position。 - Note: + 参数含义见 ``buy`` 的说明。 - - ``size``: automatically calculated from the existing position if - not provided (default: ``None``) by the caller + Args: + - ``data``: 要关闭 position 的 data。 + - ``size``: 要关闭的数量;未提供时,会根据现有 position 自动计算。 + - ``**kwargs``: 传给 ``buy`` 或 ``sell`` 的额外参数。 - Returns: the submitted order + Returns: + Order | None: 提交后的 order;没有 position 时返回 ``None``。 ''' if isinstance(data, string_types): data = self.getdatabyname(data) @@ -1004,135 +876,35 @@ def buy_bracket(self, data=None, size=None, price=None, plimit=None, stopprice=None, stopexec=bt.Order.Stop, stopargs={}, limitprice=None, limitexec=bt.Order.Limit, limitargs={}, **kwargs): - ''' - Create a bracket order group (low side - buy order - high side). The - default behavior is as follows: - - - Issue a **buy** order with execution ``Limit`` - - - Issue a *low side* bracket **sell** order with execution ``Stop`` - - - Issue a *high side* bracket **sell** order with execution - ``Limit``. - - See below for the different parameters - - - ``data`` (default: ``None``) - - For which data the order has to be created. If ``None`` then the - first data in the system, ``self.datas[0] or self.data0`` (aka - ``self.data``) will be used - - - ``size`` (default: ``None``) - - Size to use (positive) of units of data to use for the order. - - If ``None`` the ``sizer`` instance retrieved via ``getsizer`` will - be used to determine the size. - - **Note**: The same size is applied to all 3 orders of the bracket - - - ``price`` (default: ``None``) - - Price to use (live brokers may place restrictions on the actual - format if it does not comply to minimum tick size requirements) - - ``None`` is valid for ``Market`` and ``Close`` orders (the market - determines the price) - - For ``Limit``, ``Stop`` and ``StopLimit`` orders this value - determines the trigger point (in the case of ``Limit`` the trigger - is obviously at which price the order should be matched) - - - ``plimit`` (default: ``None``) - - Only applicable to ``StopLimit`` orders. This is the price at which - to set the implicit *Limit* order, once the *Stop* has been - triggered (for which ``price`` has been used) - - - ``trailamount`` (default: ``None``) - - If the order type is StopTrail or StopTrailLimit, this is an - absolute amount which determines the distance to the price (below - for a Sell order and above for a buy order) to keep the trailing - stop - - - ``trailpercent`` (default: ``None``) - - If the order type is StopTrail or StopTrailLimit, this is a - percentage amount which determines the distance to the price (below - for a Sell order and above for a buy order) to keep the trailing - stop (if ``trailamount`` is also specified it will be used) - - - ``exectype`` (default: ``bt.Order.Limit``) - - Possible values: (see the documentation for the method ``buy`` - - - ``valid`` (default: ``None``) - - Possible values: (see the documentation for the method ``buy`` - - - ``tradeid`` (default: ``0``) - - Possible values: (see the documentation for the method ``buy`` - - - ``oargs`` (default: ``{}``) - - Specific keyword arguments (in a ``dict``) to pass to the main side - order. Arguments from the default ``**kwargs`` will be applied on - top of this. - - - ``**kwargs``: additional broker implementations may support extra - parameters. ``backtrader`` will pass the *kwargs* down to the - created order objects - - Possible values: (see the documentation for the method ``buy`` - - **Note**: this ``kwargs`` will be applied to the 3 orders of a - bracket. See below for specific keyword arguments for the low and - high side orders - - - ``stopprice`` (default: ``None``) - - Specific price for the *low side* stop order - - - ``stopexec`` (default: ``bt.Order.Stop``) - - Specific execution type for the *low side* order - - - ``stopargs`` (default: ``{}``) - - Specific keyword arguments (in a ``dict``) to pass to the low side - order. Arguments from the default ``**kwargs`` will be applied on - top of this. - - - ``limitprice`` (default: ``None``) - - Specific price for the *high side* stop order - - - ``stopexec`` (default: ``bt.Order.Limit``) - - Specific execution type for the *high side* order - - - ``limitargs`` (default: ``{}``) - - Specific keyword arguments (in a ``dict``) to pass to the high side - order. Arguments from the default ``**kwargs`` will be applied on - top of this. - - High/Low Side orders can be suppressed by using: - - - ``limitexec=None`` to suppress the *high side* - - - ``stopexec=None`` to suppress the *low side* + '''创建 buy bracket order group(low side - buy order - high side)。 + + 默认行为: + + - 发出 ``Limit`` execution 的 **buy** order。 + - 发出 *low side* ``Stop`` execution 的 bracket **sell** order。 + - 发出 *high side* ``Limit`` execution 的 bracket **sell** order。 + + Args: + - ``data``: order 所属 data;为 ``None`` 时使用第 1 个 data。 + - ``size``: order size;为 ``None`` 时由 ``sizer`` 计算。bracket 的 3 个 + orders 使用同一 size。 + - ``price`` / ``plimit`` / ``trailamount`` / ``trailpercent``: + 含义见 ``buy``。 + - ``exectype`` / ``valid`` / ``tradeid``: 含义见 ``buy``。 + - ``oargs``: 传给主侧 order 的专用关键字参数,会叠加默认 ``**kwargs``。 + - ``stopprice``: *low side* stop order 的指定价格。 + - ``stopexec``: *low side* order 的 execution type;设为 ``None`` 可关闭 + *low side*。 + - ``stopargs``: 传给 *low side* order 的专用关键字参数。 + - ``limitprice``: *high side* limit order 的指定价格。 + - ``limitexec``: *high side* order 的 execution type;设为 ``None`` 可关闭 + *high side*。 + - ``limitargs``: 传给 *high side* order 的专用关键字参数。 + - ``**kwargs``: 传给 3 个 bracket orders 的额外 broker 参数。 Returns: - - - A list containing the 3 orders [order, stop side, limit side] - - - If high/low orders have been suppressed the return value will still - contain 3 orders, but those suppressed will have a value of - ``None`` + list: 包含 3 个元素 ``[order, stop side, limit side]``。如果关闭了 + high/low side,对应位置仍保留,但值为 ``None``。 ''' kargs = dict(size=size, @@ -1145,7 +917,7 @@ def buy_bracket(self, data=None, size=None, price=None, plimit=None, o = self.buy(**kargs) if stopexec is not None: - # low side / stop + # low side / stop 侧 kargs = dict(data=data, price=stopprice, exectype=stopexec, valid=valid, tradeid=tradeid) kargs.update(stopargs) @@ -1158,7 +930,7 @@ def buy_bracket(self, data=None, size=None, price=None, plimit=None, ostop = None if limitexec is not None: - # high side / limit + # high side / limit 侧 kargs = dict(data=data, price=limitprice, exectype=limitexec, valid=valid, tradeid=tradeid) kargs.update(limitargs) @@ -1180,31 +952,23 @@ def sell_bracket(self, data=None, stopprice=None, stopexec=bt.Order.Stop, stopargs={}, limitprice=None, limitexec=bt.Order.Limit, limitargs={}, **kwargs): - ''' - Create a bracket order group (low side - buy order - high side). The - default behavior is as follows: - - - Issue a **sell** order with execution ``Limit`` - - - Issue a *high side* bracket **buy** order with execution ``Stop`` + '''创建 sell bracket order group(low side - sell order - high side)。 - - Issue a *low side* bracket **buy** order with execution ``Limit``. + 默认行为: - See ``bracket_buy`` for the meaning of the parameters + - 发出 ``Limit`` execution 的 **sell** order。 + - 发出 *high side* ``Stop`` execution 的 bracket **buy** order。 + - 发出 *low side* ``Limit`` execution 的 bracket **buy** order。 - High/Low Side orders can be suppressed by using: + Args: + 参数含义与 ``buy_bracket`` 对称。 - - ``stopexec=None`` to suppress the *high side* - - - ``limitexec=None`` to suppress the *low side* + - ``stopexec=None`` 可关闭 *high side*。 + - ``limitexec=None`` 可关闭 *low side*。 Returns: - - - A list containing the 3 orders [order, stop side, limit side] - - - If high/low orders have been suppressed the return value will still - contain 3 orders, but those suppressed will have a value of - ``None`` + list: 包含 3 个元素 ``[order, stop side, limit side]``。如果关闭了 + high/low side,对应位置仍保留,但值为 ``None``。 ''' kargs = dict(size=size, @@ -1217,20 +981,20 @@ def sell_bracket(self, data=None, o = self.sell(**kargs) if stopexec is not None: - # high side / stop + # high side / stop 侧 kargs = dict(data=data, price=stopprice, exectype=stopexec, valid=valid, tradeid=tradeid) kargs.update(stopargs) kargs.update(kwargs) kargs['parent'] = o - kargs['transmit'] = limitexec is None # transmit if last + kargs['transmit'] = limitexec is None # 如果是最后一单则 transmit kargs['size'] = o.size ostop = self.buy(**kargs) else: ostop = None if limitexec is not None: - # low side / limit + # low side / limit 侧 kargs = dict(data=data, price=limitprice, exectype=limitexec, valid=valid, tradeid=tradeid) kargs.update(limitargs) @@ -1245,23 +1009,21 @@ def sell_bracket(self, data=None, return [o, ostop, olimit] def order_target_size(self, data=None, target=0, **kwargs): - ''' - Place an order to rebalance a position to have final size of ``target`` - - The current ``position`` size is taken into account as the start point - to achieve ``target`` + '''下单调仓,使 position 最终 size 达到 ``target``。 - - If ``target`` > ``pos.size`` -> buy ``target - pos.size`` + 当前 ``position`` size 会作为起点参与计算: - - If ``target`` < ``pos.size`` -> sell ``pos.size - target`` + - 如果 ``target`` > ``pos.size``,则 buy ``target - pos.size``。 + - 如果 ``target`` < ``pos.size``,则 sell ``pos.size - target``。 - It returns either: + Args: + - ``data``: 要调仓的 data。 + - ``target``: 目标 size。 + - ``**kwargs``: 传给 ``buy`` / ``sell`` / ``close`` 的额外参数。 - - The generated order - - or - - - ``None`` if no order has been issued (``target == position.size``) + Returns: + Order | None: 生成的 order;如果无需下单(``target == position.size``) + 则返回 ``None``。 ''' if isinstance(data, string_types): data = self.getdatabyname(data) @@ -1278,27 +1040,25 @@ def order_target_size(self, data=None, target=0, **kwargs): elif target < possize: return self.sell(data=data, size=possize - target, **kwargs) - return None # no execution target == possize + return None # 无需执行,target == possize def order_target_value(self, data=None, target=0.0, price=None, **kwargs): - ''' - Place an order to rebalance a position to have final value of - ``target`` - - The current ``value`` is taken into account as the start point to - achieve ``target`` + '''下单调仓,使 position 最终 value 达到 ``target``。 - - If no ``target`` then close postion on data - - If ``target`` > ``value`` then buy on data - - If ``target`` < ``value`` then sell on data + 当前 ``value`` 会作为起点参与计算: - It returns either: + - 如果没有 ``target``,则关闭该 data 上的 position。 + - 如果 ``target`` > ``value``,则在该 data 上 buy。 + - 如果 ``target`` < ``value``,则在该 data 上 sell。 - - The generated order + Args: + - ``data``: 要调仓的 data。 + - ``target``: 目标 value。 + - ``price``: 计算 size 时使用的价格;为 ``None`` 时使用当前 close。 + - ``**kwargs``: 传给 ``buy`` / ``sell`` / ``close`` 的额外参数。 - or - - - ``None`` if no order has been issued + Returns: + Order | None: 生成的 order;如果无需下单则返回 ``None``。 ''' if isinstance(data, string_types): @@ -1307,14 +1067,14 @@ def order_target_value(self, data=None, target=0.0, price=None, **kwargs): data = self.data possize = self.getposition(data, self.broker).size - if not target and possize: # closing a position + if not target and possize: # 关闭 position return self.close(data=data, size=possize, price=price, **kwargs) else: value = self.broker.getvalue(datas=[data]) comminfo = self.broker.getcommissioninfo(data) - # Make sure a price is there + # 确保存在可用价格 price = price if price is not None else data.close[0] if target > value: @@ -1325,45 +1085,29 @@ def order_target_value(self, data=None, target=0.0, price=None, **kwargs): size = comminfo.getsize(price, value - target) return self.sell(data=data, size=size, price=price, **kwargs) - return None # no execution size == possize + return None # 无需执行,size == possize def order_target_percent(self, data=None, target=0.0, **kwargs): - ''' - Place an order to rebalance a position to have final value of - ``target`` percentage of current portfolio ``value`` - - ``target`` is expressed in decimal: ``0.05`` -> ``5%`` - - It uses ``order_target_value`` to execute the order. - - Example: - - ``target=0.05`` and portfolio value is ``100`` - - - The ``value`` to be reached is ``0.05 * 100 = 5`` - - - ``5`` is passed as the ``target`` value to ``order_target_value`` - - The current ``value`` is taken into account as the start point to - achieve ``target`` - - The ``position.size`` is used to determine if a position is ``long`` / - ``short`` + '''下单调仓,使 position 最终 value 达到当前 portfolio ``value`` 的百分比。 - - If ``target`` > ``value`` - - buy if ``pos.size >= 0`` (Increase a long position) - - sell if ``pos.size < 0`` (Increase a short position) + ``target`` 使用小数表示:``0.05`` 表示 ``5%``。该方法通过 + ``order_target_value`` 执行。 - - If ``target`` < ``value`` - - sell if ``pos.size >= 0`` (Decrease a long position) - - buy if ``pos.size < 0`` (Decrease a short position) + Args: + - ``data``: 要调仓的 data。 + - ``target``: 目标百分比。 + - ``**kwargs``: 传给 ``order_target_value`` 的额外参数。 - It returns either: - - - The generated order + Returns: + Order | None: 生成的 order;如果无需下单(``target == position.size``) + 则返回 ``None``。 - or + --- + 交互界面使用示范例子: - - ``None`` if no order has been issued (``target == position.size``) + - 当 ``target=0.05`` 且 portfolio value 为 ``100`` 时,目标 value 为 + ``0.05 * 100 = 5``,随后会把 ``5`` 作为 ``target`` 传给 + ``order_target_value``。 ''' if isinstance(data, string_types): data = self.getdatabyname(data) @@ -1376,12 +1120,12 @@ def order_target_percent(self, data=None, target=0.0, **kwargs): return self.order_target_value(data=data, target=target, **kwargs) def getposition(self, data=None, broker=None): - ''' - Returns the current position for a given data in a given broker. + '''返回指定 data 在指定 broker 中的当前 position。 - If both are None, the main data and the default broker will be used + 如果两者都为 ``None``,使用主 data 和默认 broker。 - A property ``position`` is also available + Returns: + Position: 当前 position;也可通过 ``position`` property 访问。 ''' data = data if data is not None else self.datas[0] broker = broker or self.broker @@ -1390,12 +1134,12 @@ def getposition(self, data=None, broker=None): position = property(getposition) def getpositionbyname(self, name=None, broker=None): - ''' - Returns the current position for a given name in a given broker. + '''返回指定名称 data 在指定 broker 中的当前 position。 - If both are None, the main data and the default broker will be used + 如果两者都为 ``None``,使用主 data 和默认 broker。 - A property ``positionbyname`` is also available + Returns: + Position: 当前 position;也可通过 ``positionbyname`` property 访问。 ''' data = self.datas[0] if not name else self.getdatabyname(name) broker = broker or self.broker @@ -1404,12 +1148,12 @@ def getpositionbyname(self, name=None, broker=None): positionbyname = property(getpositionbyname) def getpositions(self, broker=None): - ''' - Returns the current by data positions directly from the broker + '''直接从 broker 返回按 data 索引的当前 positions。 - If the given ``broker`` is None, the default broker will be used + 如果 ``broker`` 为 ``None``,使用默认 broker。 - A property ``positions`` is also available + Returns: + dict: 当前 positions;也可通过 ``positions`` property 访问。 ''' broker = broker or self.broker return broker.positions @@ -1417,12 +1161,12 @@ def getpositions(self, broker=None): positions = property(getpositions) def getpositionsbyname(self, broker=None): - ''' - Returns the current by name positions directly from the broker + '''直接从 broker 返回按名称索引的当前 positions。 - If the given ``broker`` is None, the default broker will be used + 如果 ``broker`` 为 ``None``,使用默认 broker。 - A property ``positionsbyname`` is also available + Returns: + OrderedDict: 当前 positions;也可通过 ``positionsbyname`` property 访问。 ''' broker = broker or self.broker positions = broker.positions @@ -1442,29 +1186,22 @@ def _addsizer(self, sizer, *args, **kwargs): self.setsizer(sizer(*args, **kwargs)) def setsizer(self, sizer): - ''' - Replace the default (fixed stake) sizer - ''' + '''替换默认 fixed stake sizer。''' self._sizer = sizer sizer.set(self, self.broker) return sizer def getsizer(self): - ''' - Returns the sizer which is in used if automatic statke calculation is - used + '''返回自动 stake 计算时使用的 sizer。 - Also available as ``sizer`` + 也可通过 ``sizer`` property 访问。 ''' return self._sizer sizer = property(getsizer, setsizer) def getsizing(self, data=None, isbuy=True): - ''' - Return the stake calculated by the sizer instance for the current - situation - ''' + '''返回当前情境下由 sizer 实例计算出的 stake。''' data = data if data is not None else self.datas[0] return self._sizer.getsizing(data, isbuy=isbuy) @@ -1472,13 +1209,13 @@ def getsizing(self, data=None, isbuy=True): class MetaSigStrategy(Strategy.__class__): def __new__(meta, name, bases, dct): - # map user defined next to custom to be able to call own method before + # 将用户定义的 next 映射为 custom,以便先调用自身方法 if 'next' in dct: dct['_next_custom'] = dct.pop('next') cls = super(MetaSigStrategy, meta).__new__(meta, name, bases, dct) - # after class creation remap _next_catch to be next + # class 创建后,将 _next_catch 重新映射为 next cls.next = cls._next_catch return cls @@ -1509,7 +1246,7 @@ def dopostinit(cls, _obj, *args, **kwargs): for sigtype, sigcls, sigargs, sigkwargs in _obj.p.signals: _obj._signals[sigtype].append(sigcls(*sigargs, **sigkwargs)) - # Record types of signals + # 记录 signal 类型 _obj._longshort = bool(_obj._signals[bt.SIGNAL_LONGSHORT]) _obj._long = bool(_obj._signals[bt.SIGNAL_LONG]) @@ -1522,84 +1259,70 @@ def dopostinit(cls, _obj, *args, **kwargs): class SignalStrategy(with_metaclass(MetaSigStrategy, Strategy)): - '''This subclass of ``Strategy`` is meant to to auto-operate using - **signals**. + '''使用 **signals** 自动操作的 ``Strategy`` 子类。 - *Signals* are usually indicators and the expected output values: + *Signals* 通常是 indicators,期望输出值含义如下: - - ``> 0`` is a ``long`` indication + - ``> 0`` 表示 ``long`` 信号。 - - ``< 0`` is a ``short`` indication + - ``< 0`` 表示 ``short`` 信号。 - There are 5 types of *Signals*, broken in 2 groups. + *Signals* 分为 2 组。 - **Main Group**: + **Main Group**: - - ``LONGSHORT``: both ``long`` and ``short`` indications from this signal - are taken + - ``LONGSHORT``: 同时接受该 signal 的 ``long`` 与 ``short`` 指示。 - ``LONG``: - - ``long`` indications are taken to go long - - ``short`` indications are taken to *close* the long position. But: + - ``long`` 指示用于做多。 + - ``short`` 指示用于 *close* long position。但: - - If a ``LONGEXIT`` (see below) signal is in the system it will be - used to exit the long + - 如果系统中存在 ``LONGEXIT``(见下方)signal,则使用它退出 long。 - - If a ``SHORT`` signal is available and no ``LONGEXIT`` is available - , it will be used to close a ``long`` before opening a ``short`` + - 如果存在 ``SHORT`` signal 且不存在 ``LONGEXIT``,则先用它关闭 ``long``, + 再打开 ``short``。 - ``SHORT``: - - ``short`` indications are taken to go short - - ``long`` indications are taken to *close* the short position. But: + - ``short`` 指示用于做空。 + - ``long`` 指示用于 *close* short position。但: - - If a ``SHORTEXIT`` (see below) signal is in the system it will be - used to exit the short + - 如果系统中存在 ``SHORTEXIT``(见下方)signal,则使用它退出 short。 - - If a ``LONG`` signal is available and no ``SHORTEXIT`` is available - , it will be used to close a ``short`` before opening a ``long`` + - 如果存在 ``LONG`` signal 且不存在 ``SHORTEXIT``,则先用它关闭 ``short``, + 再打开 ``long``。 - **Exit Group**: + **Exit Group**: - This 2 signals are meant to override others and provide criteria for - exitins a ``long``/``short`` position + 这 2 类 signals 用于覆盖其他 signals,并为退出 ``long`` / ``short`` position + 提供条件。 - - ``LONGEXIT``: ``short`` indications are taken to exit ``long`` - positions + - ``LONGEXIT``: ``short`` 指示用于退出 ``long`` positions。 - - ``SHORTEXIT``: ``long`` indications are taken to exit ``short`` - positions + - ``SHORTEXIT``: ``long`` 指示用于退出 ``short`` positions。 **Order Issuing** - Orders execution type is ``Market`` and validity is ``None`` (*Good until - Canceled*) - - Params: - - - ``signals`` (default: ``[]``): a list/tuple of lists/tuples that allows - the instantiation of the signals and allocation to the right type - - This parameter is expected to be managed through ``cerebro.add_signal`` - - - ``_accumulate`` (default: ``False``): allow to enter the market - (long/short) even if already in the market - - - ``_concurrent`` (default: ``False``): allow orders to be issued even if - orders are already pending execution + orders 的 execution type 为 ``Market``,validity 为 ``None``(*Good until + Canceled*)。 - - ``_data`` (default: ``None``): if multiple datas are present in the - system which is the target for orders. This can be + Args: + - ``signals``: list/tuple,元素也是 list/tuple,用于实例化 signals 并分配到 + 正确类型。通常由 ``cerebro.add_signal`` 管理。 - - ``None``: The first data in the system will be used + - ``_accumulate``: 已在市场中时,是否仍允许继续进入市场(long/short)。 - - An ``int``: indicating the data that was inserted at that position + - ``_concurrent``: 已有 orders pending execution 时,是否仍允许继续发出 + orders。 - - An ``str``: name given to the data when creating it (parameter - ``name``) or when adding it cerebro with ``cerebro.adddata(..., - name=)`` + - ``_data``: 多 datas 场景下 orders 的目标 data。可以是: - - A ``data`` instance + - ``None``: 使用系统中的第 1 个 data。 + - ``int``: 使用插入在该位置的 data。 + - ``str``: 使用创建/添加 data 时传入的 ``name``。 + - ``data`` 实例。 + Returns: + SignalStrategy: 可根据 signals 自动发出 market orders 的 strategy。 ''' params = ( @@ -1610,14 +1333,14 @@ class SignalStrategy(with_metaclass(MetaSigStrategy, Strategy)): ) def _start(self): - self._sentinel = None # sentinel for order concurrency + self._sentinel = None # order concurrency 的 sentinel super(SignalStrategy, self)._start() def signal_add(self, sigtype, signal): self._signals[sigtype].append(signal) def _notify(self, qorders=[], qtrades=[]): - # Nullify the sentinel if done + # 如果 order 已完成,则清空 sentinel procorders = qorders or self._orderspending if self._sentinel is not None: for order in procorders: @@ -1634,12 +1357,12 @@ def _next_catch(self): def _next_signal(self): if self._sentinel is not None and not self.p._concurrent: - return # order active and more than 1 not allowed + return # order 仍 active,且不允许超过 1 个 sigs = self._signals nosig = [[0.0]] - # Calculate current status of the signals + # 计算 signals 当前状态 ls_long = all(x[0] > 0.0 for x in sigs[bt.SIGNAL_LONGSHORT] or nosig) ls_short = all(x[0] < 0.0 for x in sigs[bt.SIGNAL_LONGSHORT] or nosig) @@ -1663,12 +1386,11 @@ def _next_signal(self): s_ex2 = all(x[0] for x in sigs[bt.SIGNAL_SHORTEXIT_ANY] or nosig) s_exit = s_ex0 or s_ex1 or s_ex2 - # Use oppossite signales to start reversal (by closing) - # but only if no "xxxExit" exists + # 仅在不存在 "xxxExit" 时,使用反向 signals 启动 reversal(先关闭) l_rev = not self._longexit and s_enter s_rev = not self._shortexit and l_enter - # Opposite of individual long and short + # 单独 long/short signal 的反向 l_leav0 = all(x[0] < 0.0 for x in sigs[bt.SIGNAL_LONG] or nosig) l_leav1 = all(x[0] > 0.0 for x in sigs[bt.SIGNAL_LONG_INV] or nosig) l_leav2 = all(x[0] for x in sigs[bt.SIGNAL_LONG_ANY] or nosig) @@ -1679,12 +1401,12 @@ def _next_signal(self): s_leav2 = all(x[0] for x in sigs[bt.SIGNAL_SHORT_ANY] or nosig) s_leave = s_leav0 or s_leav1 or s_leav2 - # Invalidate long leave if longexit signals are available + # 如果存在 longexit signals,则让 long leave 失效 l_leave = not self._longexit and l_leave - # Invalidate short leave if shortexit signals are available + # 如果存在 shortexit signals,则让 short leave 失效 s_leave = not self._shortexit and s_leave - # Take size and start logic + # 获取 size 并开始信号逻辑 size = self.getposition(self._dtarget).size if not size: if ls_long or l_enter: @@ -1693,9 +1415,9 @@ def _next_signal(self): elif ls_short or s_enter: self._sentinel = self.sell(self._dtarget) - elif size > 0: # current long position + elif size > 0: # 当前 long position if ls_short or l_exit or l_rev or l_leave: - # closing position - not relevant for concurrency + # 关闭 position,与 concurrency 无关 self.close(self._dtarget) if ls_short or l_rev: @@ -1705,9 +1427,9 @@ def _next_signal(self): if self.p._accumulate: self._sentinel = self.buy(self._dtarget) - elif size < 0: # current short position + elif size < 0: # 当前 short position if ls_long or s_exit or s_rev or s_leave: - # closing position - not relevant for concurrency + # 关闭 position,与 concurrency 无关 self.close(self._dtarget) if ls_long or s_rev: diff --git a/backtrader/studies/contrib/fractal.py b/backtrader/studies/contrib/fractal.py index 8cb644e80..83c263c12 100644 --- a/backtrader/studies/contrib/fractal.py +++ b/backtrader/studies/contrib/fractal.py @@ -28,10 +28,29 @@ class Fractal(bt.ind.PeriodN): - ''' + '''Fractal indicator。 + + 在指定 period 内,如果最高 high 位于中心且两侧 high 更低,则标记 bearish + fractal;如果最低 low 位于中心且两侧 low 更高,则标记 bullish fractal。 + + Args: + period: 判断 fractal pattern 使用的 bar 数量。 + bardist: 绘制 marker 时相对 high/low 的偏移百分比。 + shift_to_potential_fractal: 中心候选 bar 相对窗口的位置。 + + Returns: + Fractal: 输出 ``fractal_bearish`` 与 ``fractal_bullish`` 两条 line 的 + indicator。 + References: [Ref 1] http://www.investopedia.com/articles/trading/06/fractals.asp + --- + 交互示例: + >>> from backtrader.studies.contrib.fractal import Fractal + >>> Fractal.params.period + 5 + ''' lines = ('fractal_bearish', 'fractal_bullish') @@ -45,13 +64,12 @@ class Fractal(bt.ind.PeriodN): ) params = ( ('period', 5), - ('bardist', 0.015), # distance to max/min in absolute perc + ('bardist', 0.015), # 到 max/min 的绝对百分比距离 ('shift_to_potential_fractal', 2), ) def next(self): - # A bearish turning point occurs when there is a pattern with the - # highest high in the middle and two lower highs on each side. [Ref 1] + # bearish turning point:最高 high 位于中间,两侧各有两个更低 high。[Ref 1] last_five_highs = self.data.high.get(size=self.p.period) max_val = max(last_five_highs) @@ -60,8 +78,7 @@ def next(self): if max_idx == self.p.shift_to_potential_fractal: self.lines.fractal_bearish[-2] = max_val * (1 + self.p.bardist) - # A bullish turning point occurs when there is a pattern with the - # lowest low in the middle and two higher lowers on each side. [Ref 1] + # bullish turning point:最低 low 位于中间,两侧各有两个更高 low。[Ref 1] last_five_lows = self.data.low.get(size=self.p.period) min_val = min(last_five_lows) min_idx = last_five_lows.index(min_val) diff --git a/backtrader/talib.py b/backtrader/talib.py index eff5ced30..acfda5893 100644 --- a/backtrader/talib.py +++ b/backtrader/talib.py @@ -21,8 +21,8 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) -# The modules below should/must define __all__ with the objects wishes -# or prepend an "_" (underscore) to private classes/variables +# 下方模块应/必须通过 __all__ 定义希望导出的对象, +# 或为私有 class/variable 添加 "_"(underscore)前缀。 import sys @@ -33,14 +33,14 @@ try: import talib except ImportError: - __all__ = [] # talib is not available + __all__ = [] # talib 不可用 else: - import numpy as np # talib dependency + import numpy as np # talib 依赖 import talib.abstract MA_Type = talib.MA_Type - # Reverse TA_FUNC_FLAGS dict + # 反转 TA_FUNC_FLAGS dict R_TA_FUNC_FLAGS = dict( zip(talib.abstract.TA_FUNC_FLAGS.values(), talib.abstract.TA_FUNC_FLAGS.keys())) @@ -60,21 +60,23 @@ OUT_FLAGS_UPPER = 2048 OUT_FLAGS_LOWER = 4096 - # Generate all indicators as subclasses + # 将所有 indicator 生成为 subclass class _MetaTALibIndicator(bt.Indicator.__class__): + '''TA-Lib indicator metaclass 的基类,用于完成 lookback 和函数绑定。''' + _refname = '_taindcol' _taindcol = dict() _KNOWN_UNSTABLE = ['SAR'] def dopostinit(cls, _obj, *args, **kwargs): - # Go to parent + # 调用 parent res = super(_MetaTALibIndicator, cls).dopostinit(_obj, *args, **kwargs) _obj, args, kwargs = res - # Get the minimum period by using the abstract interface and params + # 通过 abstract interface 和 params 获取 minimum period _obj._tabstract.set_function_args(**_obj.p._getkwargs()) _obj._lookback = lookback = _obj._tabstract.lookback + 1 _obj.updateminperiod(lookback) @@ -87,25 +89,29 @@ def dopostinit(cls, _obj, *args, **kwargs): cerebro = bt.metabase.findowner(_obj, bt.Cerebro) tafuncinfo = _obj._tabstract.info _obj._tafunc = getattr(talib, tafuncinfo['name'], None) - return _obj, args, kwargs # return the object and args + return _obj, args, kwargs # 返回 object 和 args class _TALibIndicator(with_metaclass(_MetaTALibIndicator, bt.Indicator)): - CANDLEOVER = 1.02 # 2% over + '''TA-Lib indicator 的基类,用于动态生成对应 backtrader indicator。''' + + CANDLEOVER = 1.02 # 上方 2% CANDLEREF = 1 # Open, High, Low, Close (0, 1, 2, 3) @classmethod def _subclass(cls, name): - # Module where the class has to end (namely this one) + '''按 TA-Lib 函数名动态创建 indicator subclass。''' + + # class 最终所在的 module(即当前 module) clsmodule = sys.modules[cls.__module__] - # Create an abstract interface to get lines names + # 创建 abstract interface 以获取 lines names _tabstract = talib.abstract.Function(name) - # Variables about the the info learnt from func_flags + # 从 func_flags 中得到的信息 iscandle = False unstable = False - # Prepare plotinfo + # 准备 plotinfo plotinfo = dict() fflags = _tabstract.function_flags or [] for fflag in fflags: @@ -119,7 +125,7 @@ def _subclass(cls, name): plotinfo['plotlinelabels'] = True iscandle = True - # Prepare plotlines + # 准备 plotlines lines = _tabstract.output_names output_flags = _tabstract.output_flags plotlines = dict() @@ -133,7 +139,7 @@ def _subclass(cls, name): if not iscandle: pline['ls'] = '-' else: - pline['_plotskip'] = True # do not plot candles + pline['_plotskip'] = True # 不绘制 candles elif orflag & OUT_FLAGS_DASH: pline['ls'] = '--' @@ -149,19 +155,17 @@ def _subclass(cls, name): samecolor = False elif orflag & OUT_FLAGS_UPPER: - samecolor = True # last: other values in loop are seen + samecolor = True # last:loop 中的其它值已被处理 - if pline: # the dict has something + if pline: # dict 中已有内容 plotlines[lname] = pline if iscandle: - # This is the line that will be plotted when the output of the - # indicator is a candle. The values of a candle (100) will be - # used to plot a sign above the maximum of the bar which - # produces the candle + # 当 indicator 输出为 candle 时绘制这条 line。 + # candle 的值(100)会用于在产生 candle 的 bar 最大值上方绘制标记。 pline = dict() - pline['_name'] = name # plotted name - lname = '_candleplot' # change name + pline['_name'] = name # 绘制名称 + lname = '_candleplot' # 修改名称 lines.append(lname) pline['ls'] = '' pline['marker'] = 'd' @@ -169,11 +173,11 @@ def _subclass(cls, name): pline['fillstyle'] = 'full' plotlines[lname] = pline - # Prepare dictionary for subclassing + # 准备用于 subclassing 的 dictionary clsdict = { '__module__': cls.__module__, '__doc__': str(_tabstract), - '_tabstract': _tabstract, # keep ref for lookback calcs + '_tabstract': _tabstract, # 保留引用,用于 lookback 计算 '_iscandle': iscandle, '_unstable': unstable, 'params': _tabstract.get_parameters(), @@ -182,25 +186,25 @@ def _subclass(cls, name): 'plotlines': plotlines, } newcls = type(str(name), (cls,), clsdict) # subclass - setattr(clsmodule, str(name), newcls) # add to module + setattr(clsmodule, str(name), newcls) # 添加到 module def oncestart(self, start, end): - pass # if not ... a call with a single value to once will happen + pass # 否则 once 会收到单个 value 调用 def once(self, start, end): import array - # prepare the data arrays - single shot + # 准备 data arrays,一次性计算 narrays = [np.array(x.lines[0].array) for x in self.datas] - # Execute + # 执行 output = self._tafunc(*narrays, **self.p._getkwargs()) fsize = self.size() lsize = fsize - self._iscandle - if lsize == 1: # only 1 output, no tuple returned + if lsize == 1: # 只有 1 个 output,不返回 tuple self.lines[0].array = array.array(str('d'), output) - if fsize > lsize: # candle is present + if fsize > lsize: # 存在 candle candleref = narrays[self.CANDLEREF] * self.CANDLEOVER output2 = candleref * (output / 100.0) self.lines[1].array = array.array(str('d'), output2) @@ -210,7 +214,7 @@ def once(self, start, end): self.lines[i].array = array.array(str('d'), o) def next(self): - # prepare the data arrays - single shot + # 准备 data arrays,一次性计算 size = self._lookback or len(self) narrays = [np.array(x.lines[0].get(size=size)) for x in self.datas] @@ -218,10 +222,10 @@ def next(self): fsize = self.size() lsize = fsize - self._iscandle - if lsize == 1: # only 1 output, no tuple returned + if lsize == 1: # 只有 1 个 output,不返回 tuple self.lines[0][0] = o = out[-1] - if fsize > lsize: # candle is present + if fsize > lsize: # 存在 candle candleref = narrays[self.CANDLEREF][-1] * self.CANDLEOVER o2 = candleref * (o / 100.0) self.lines[1][0] = o2 @@ -230,7 +234,7 @@ def next(self): for i, o in enumerate(out): self.lines[i][0] = o[-1] - # When importing the module do an automatic declaration of thed + # import module 时自动声明所有 TA-Lib 函数对应的 indicator tafunctions = talib.get_functions() for tafunc in tafunctions: _TALibIndicator._subclass(tafunc) diff --git a/backtrader/timer.py b/backtrader/timer.py index a77706a5b..9728483ac 100644 --- a/backtrader/timer.py +++ b/backtrader/timer.py @@ -40,6 +40,8 @@ class Timer(with_metaclass(MetaParams, object)): + '''Timer 的基类,用于按 session、日期过滤和 repeat 规则触发回调。''' + params = ( ('tid', None), ('owner', None), @@ -51,7 +53,7 @@ class Timer(with_metaclass(MetaParams, object)): ('weekcarry', False), ('monthdays', []), ('monthcarry', True), - ('allow', None), # callable that allows a timer to take place + ('allow', None), # 允许 timer 触发的 callable ('tzdata', None), ('cheat', False), ) @@ -63,7 +65,8 @@ def __init__(self, *args, **kwargs): self.kwargs = kwargs def start(self, data): - # write down the 'reset when' value + '''启动 timer,并根据 data/session 信息初始化下一次触发时间。''' + # 记录 'reset when' 值 if not isinstance(self.p.when, integer_types): # expect time/datetime self._rstwhen = self.p.when self._tzdata = self.p.tzdata @@ -81,19 +84,21 @@ def start(self, data): self._nexteos = datetime.min self._curdate = date.min - self._curmonth = -1 # non-existent month + self._curmonth = -1 # 不存在的月份 self._monthmask = collections.deque() - self._curweek = -1 # non-existent week + self._curweek = -1 # 不存在的周 self._weekmask = collections.deque() def _reset_when(self, ddate=datetime.min): + '''重置当前目标触发时间。''' self._when = self._rstwhen self._dtwhen = self._dwhen = None self._lastcall = ddate def _check_month(self, ddate): + '''检查当前日期是否满足 monthdays/monthcarry 条件。''' if not self.p.monthdays: return True @@ -101,15 +106,15 @@ def _check_month(self, ddate): daycarry = False dmonth = ddate.month if dmonth != self._curmonth: - self._curmonth = dmonth # write down new month + self._curmonth = dmonth # 记录新月份 daycarry = self.p.monthcarry and bool(mask) self._monthmask = mask = collections.deque(self.p.monthdays) dday = ddate.day - dc = bisect.bisect_left(mask, dday) # "left" for days before dday + dc = bisect.bisect_left(mask, dday) # "left" 对应 dday 之前的天 daycarry = daycarry or (self.p.monthcarry and dc > 0) if dc < len(mask): - curday = bisect.bisect_right(mask, dday, lo=dc) > 0 # check dday + curday = bisect.bisect_right(mask, dday, lo=dc) > 0 # 检查 dday dc += curday else: curday = False @@ -121,6 +126,7 @@ def _check_month(self, ddate): return daycarry or curday def _check_week(self, ddate=date.min): + '''检查当前日期是否满足 weekdays/weekcarry 条件。''' if not self.p.weekdays: return True @@ -129,14 +135,14 @@ def _check_week(self, ddate=date.min): mask = self._weekmask daycarry = False if dweek != self._curweek: - self._curweek = dweek # write down new month + self._curweek = dweek # 记录新周 daycarry = self.p.weekcarry and bool(mask) self._weekmask = mask = collections.deque(self.p.weekdays) - dc = bisect.bisect_left(mask, dwkday) # "left" for days before dday + dc = bisect.bisect_left(mask, dwkday) # "left" 对应 dwkday 之前的天 daycarry = daycarry or (self.p.weekcarry and dc > 0) if dc < len(mask): - curday = bisect.bisect_right(mask, dwkday, lo=dc) > 0 # check dday + curday = bisect.bisect_right(mask, dwkday, lo=dc) > 0 # 检查 dwkday dc += curday else: curday = False @@ -148,20 +154,21 @@ def _check_week(self, ddate=date.min): return daycarry or curday def check(self, dt): + '''检查给定 numeric datetime 是否触发 timer。''' d = num2date(dt) ddate = d.date() - if self._lastcall == ddate: # not repeating, awaiting date change + if self._lastcall == ddate: # 非 repeat,等待日期变化 return False if d > self._nexteos: - if self._isdata: # eos provided by data + if self._isdata: # eos 由 data 提供 nexteos, _ = self._tzdata._getnexteos() - else: # generic eos + else: # 通用 eos nexteos = datetime.combine(ddate, TIME_MAX) self._nexteos = nexteos self._reset_when() - if ddate > self._curdate: # day change + if ddate > self._curdate: # 日期变化 self._curdate = ddate ret = self._check_month(ddate) if ret: @@ -170,10 +177,10 @@ def check(self, dt): ret = self.p.allow(ddate) if not ret: - self._reset_when(ddate) # this day won't make it - return False # timer target not met + self._reset_when(ddate) # 这一天不会触发 + return False # timer target 未满足 - # no day change or passed month, week and allow filters on date change + # 无日期变化,或日期变化时已通过 month、week 和 allow filters dwhen = self._dwhen dtwhen = self._dtwhen if dtwhen is None: @@ -189,17 +196,17 @@ def check(self, dt): self._dtwhen = dtwhen = date2num(dwhen, tz=self._tzdata) if dt < dtwhen: - return False # timer target not met + return False # timer target 未满足 - self.lastwhen = dwhen # record when the last timer "when" happened + self.lastwhen = dwhen # 记录上次 timer "when" 发生的时间 - if not self.p.repeat: # cannot repeat - self._reset_when(ddate) # reset and mark as called on ddate + if not self.p.repeat: # 不可 repeat + self._reset_when(ddate) # reset 并标记 ddate 已调用 else: if d > self._nexteos: - if self._isdata: # eos provided by data + if self._isdata: # eos 由 data 提供 nexteos, _ = self._tzdata._getnexteos() - else: # generic eos + else: # 通用 eos nexteos = datetime.combine(ddate, TIME_MAX) self._nexteos = nexteos @@ -208,18 +215,18 @@ def check(self, dt): while True: dwhen += self.p.repeat - if dwhen > nexteos: # new schedule is beyone session - self._reset_when(ddate) # reset to original point + if dwhen > nexteos: # 新 schedule 超过 session + self._reset_when(ddate) # reset 到原始点 break - if dwhen > d: # gone over current datetime + if dwhen > d: # 已超过当前 datetime self._dtwhen = dtwhen = date2num(dwhen) # float timestamp - # Get the localized expected next time + # 获取 localized 后的预期 next time if self._isdata: self._dwhen = self._tzdata.num2date(dtwhen) - else: # assume pytz compatible or None + else: # 假设为 pytz compatible 或 None self._dwhen = num2date(dtwhen, tz=self._tzdata) break - return True # timer target was met + return True # timer target 已满足 diff --git a/backtrader/trade.py b/backtrader/trade.py index 8bbf5e380..d9825f6bc 100644 --- a/backtrader/trade.py +++ b/backtrader/trade.py @@ -29,35 +29,57 @@ class TradeHistory(AutoOrderedDict): - '''Represents the status and update event for each update a Trade has + '''表示 Trade 每次 update 后的状态和事件信息。 - This object is a dictionary which allows '.' notation + 该对象是一个支持 ``.`` 访问语法的 dictionary。 Attributes: - - ``status`` (``dict`` with '.' notation): Holds the resulting status of - an update event and has the following sub-attributes + - ``status`` (支持 ``.`` 访问的 ``dict``): 保存 update 事件后的状态, + 包含以下子属性 - ``status`` (``int``): Trade status - - ``dt`` (``float``): float coded datetime - - ``barlen`` (``int``): number of bars the trade has been active - - ``size`` (``int``): current size of the Trade - - ``price`` (``float``): current price of the Trade - - ``value`` (``float``): current monetary value of the Trade - - ``pnl`` (``float``): current profit and loss of the Trade - - ``pnlcomm`` (``float``): current profit and loss minus commission - - - ``event`` (``dict`` with '.' notation): Holds the event update - - parameters - - - ``order`` (``object``): the order which initiated the``update`` - - ``size`` (``int``): size of the update - - ``price`` (``float``):price of the update - - ``commission`` (``float``): price of the update + - ``dt`` (``float``): float 编码的 datetime + - ``barlen`` (``int``): trade 已活跃的 bar 数量 + - ``size`` (``int``): Trade 当前 size + - ``price`` (``float``): Trade 当前 price + - ``value`` (``float``): Trade 当前货币 value + - ``pnl`` (``float``): Trade 当前 profit and loss + - ``pnlcomm`` (``float``): 扣除 commission 后的 profit and loss + + - ``event`` (支持 ``.`` 访问的 ``dict``): 保存事件 update 参数 + + - ``order`` (``object``): 触发 ``update`` 的 order + - ``size`` (``int``): update 的 size + - ``price`` (``float``): update 的 price + - ``commission`` (``float``): update 的 commission + + --- + 交互示例: + + >>> hist = TradeHistory(Trade.Open, 0.0, 3, 10, 100.0, 1000.0, 5.0, 4.0, None) + >>> hist.status.size + 10 + >>> hist.doupdate(order=None, size=10, price=100.0, commission=1.0) + >>> hist.event.commission + 1.0 ''' def __init__(self, status, dt, barlen, size, price, value, pnl, pnlcomm, tz, event=None): - '''Initializes the object to the current status of the Trade''' + '''初始化为 Trade 的当前状态。 + + Args: + status (int): Trade status。 + dt (float): float 编码的 datetime。 + barlen (int): trade 已活跃的 bar 数量。 + size (int): Trade 当前 size。 + price (float): Trade 当前 price。 + value (float): Trade 当前 value。 + pnl (float): 当前 gross pnl。 + pnlcomm (float): 扣除 commission 后的 net pnl。 + tz: 与 ``dt`` 配套使用的 timezone。 + event: 可选的 event 数据,会保存到 ``self.event``。 + ''' super(TradeHistory, self).__init__() self.status.status = status self.status.dt = dt @@ -77,72 +99,91 @@ def __reduce__(self): self.status.tz, self.event, )) def doupdate(self, order, size, price, commission): - '''Used to fill the ``update`` part of the history entry''' + '''填充 history entry 中的 ``update`` 事件部分。 + + Args: + order: 触发本次 update 的 order。 + size (int): 本次 update 的 size。 + price (float): 本次 update 的 price。 + commission (float): 本次 update 产生的 commission。 + ''' self.event.order = order self.event.size = size self.event.price = price self.event.commission = commission - # Do not allow updates (avoids typing errors) + # 不再允许继续更新,避免误写字段 self._close() def datetime(self, tz=None, naive=True): - '''Returns a datetime for the time the update event happened''' + '''返回本次 update 事件发生时的 datetime。 + + Args: + tz: 可选 timezone。未提供时使用 history 中保存的 timezone。 + naive (bool): 是否返回 naive datetime。 + + Returns: + datetime.datetime: update 事件发生时间。 + ''' return num2date(self.status.dt, tz or self.status.tz, naive) class Trade(object): - '''Keeps track of the life of an trade: size, price, - commission (and value?) + '''跟踪一个 trade 的生命周期:size、price、commission 和 value。 + + trade 从 0 开始,可以增加和减少;当 size 回到 0 时视为 closed。 - An trade starts at 0 can be increased and reduced and can - be considered closed if it goes back to 0. + trade 可以是 long(正 size)或 short(负 size)。 - The trade can be long (positive size) or short (negative size) + Trade 不用于表达反转,内部逻辑也不支持反转。 - An trade is not meant to be reversed (no support in the logic for it) + 成员属性: - Member Attributes: + - ``ref``: 唯一 trade 标识符 + - ``status`` (``int``): Created、Open、Closed 之一 + - ``tradeid``: 创建 order 时传入的分组 tradeid。order 中默认值为 0 + - ``size`` (``int``): trade 当前 size + - ``price`` (``float``): trade 当前 price + - ``value`` (``float``): trade 当前 value + - ``commission`` (``float``): 当前累计 commission + - ``pnl`` (``float``): trade 当前 profit and loss(gross pnl) + - ``pnlcomm`` (``float``): trade 当前扣除 commission 后的 profit and loss + (net pnl) + - ``isclosed`` (``bool``): 记录最后一次 update 是否关闭了 trade + (将 size 设为 0) + - ``isopen`` (``bool``): 记录是否有任何 update 打开了 trade + - ``justopened`` (``bool``): trade 是否刚刚打开 + - ``baropen`` (``int``): 该 trade 打开时所在的 bar - - ``ref``: unique trade identifier - - ``status`` (``int``): one of Created, Open, Closed - - ``tradeid``: grouping tradeid passed to orders during creation - The default in orders is 0 - - ``size`` (``int``): current size of the trade - - ``price`` (``float``): current price of the trade - - ``value`` (``float``): current value of the trade - - ``commission`` (``float``): current accumulated commission - - ``pnl`` (``float``): current profit and loss of the trade (gross pnl) - - ``pnlcomm`` (``float``): current profit and loss of the trade minus - commission (net pnl) - - ``isclosed`` (``bool``): records if the last update closed (set size to - null the trade - - ``isopen`` (``bool``): records if any update has opened the trade - - ``justopened`` (``bool``): if the trade was just opened - - ``baropen`` (``int``): bar in which this trade was opened + - ``dtopen`` (``float``): trade 打开时的 float 编码 datetime - - ``dtopen`` (``float``): float coded datetime in which the trade was - opened + - 使用 ``open_datetime`` 获取 Python ``datetime.datetime``,或使用平台 + 提供的 ``num2date`` 方法 - - Use method ``open_datetime`` to get a Python datetime.datetime - or use the platform provided ``num2date`` method + - ``barclose`` (``int``): 该 trade 关闭时所在的 bar - - ``barclose`` (``int``): bar in which this trade was closed + - ``dtclose`` (``float``): trade 关闭时的 float 编码 datetime - - ``dtclose`` (``float``): float coded datetime in which the trade was - closed + - 使用 ``close_datetime`` 获取 Python ``datetime.datetime``,或使用平台 + 提供的 ``num2date`` 方法 - - Use method ``close_datetime`` to get a Python datetime.datetime - or use the platform provided ``num2date`` method + - ``barlen`` (``int``): 该 trade 保持 open 的 bar 数量 + - ``historyon`` (``bool``): 是否记录 history + - ``history`` (``list``): 随每次 "update" 事件更新的列表,包含 update 后的 + 状态和 update 使用的参数 - - ``barlen`` (``int``): number of bars this trade was open - - ``historyon`` (``bool``): whether history has to be recorded - - ``history`` (``list``): holds a list updated with each "update" event - containing the resulting status and parameters used in the update + history 中第一个 entry 是 Opening Event,最后一个 entry 是 Closing Event - The first entry in the history is the Opening Event - The last entry in the history is the Closing Event + --- + 交互示例: + >>> trade = Trade(size=10, price=100.0, value=1000.0, commission=1.5) + >>> trade.size, trade.price, trade.commission + (10, 100.0, 1.5) + >>> len(trade) + 10 + >>> bool(trade) + True ''' refbasis = itertools.count(1) @@ -164,6 +205,18 @@ def __str__(self): def __init__(self, data=None, tradeid=0, historyon=False, size=0, price=0.0, value=0.0, commission=0.0): + ''' + 创建一个 Trade 实例。 + + Args: + data: 与 trade 关联的 data feed。 + tradeid (int): 用于分组 order/trade 的标识符。 + historyon (bool): 是否记录每次 update 的 history。 + size (int): 初始 trade size。 + price (float): 初始 trade price。 + value (float): 初始 trade value。 + commission (float): 初始累计 commission。 + ''' self.ref = next(self.refbasis) self.data = data @@ -192,73 +245,92 @@ def __init__(self, data=None, tradeid=0, historyon=False, self.status = self.Created def __len__(self): - '''Absolute size of the trade''' + '''返回 trade 的绝对 size。 + + Returns: + int: ``abs(self.size)``。 + ''' return abs(self.size) def __bool__(self): - '''Trade size is not 0''' + '''判断 trade size 是否非零。 + + Returns: + bool: 如果 ``size != 0`` 则返回 ``True``。 + ''' return self.size != 0 __nonzero__ = __bool__ def getdataname(self): - '''Shortcut to retrieve the name of the data this trade references''' + '''获取该 trade 引用的 data 名称。 + + Returns: + str: ``self.data._name``。 + ''' return self.data._name def open_datetime(self, tz=None, naive=True): - '''Returns a datetime.datetime object with the datetime in which - the trade was opened + '''返回 trade 打开时间对应的 ``datetime.datetime``。 + + Args: + tz: 可选 timezone。 + naive (bool): 是否返回 naive datetime。 + + Returns: + datetime.datetime: trade 打开时间。 ''' return self.data.num2date(self.dtopen, tz=tz, naive=naive) def close_datetime(self, tz=None, naive=True): - '''Returns a datetime.datetime object with the datetime in which - the trade was closed + '''返回 trade 关闭时间对应的 ``datetime.datetime``。 + + Args: + tz: 可选 timezone。 + naive (bool): 是否返回 naive datetime。 + + Returns: + datetime.datetime: trade 关闭时间。 ''' return self.data.num2date(self.dtclose, tz=tz, naive=naive) def update(self, order, size, price, value, commission, pnl, comminfo): ''' - Updates the current trade. The logic does not check if the - trade is reversed, which is not conceptually supported by the - object. + 更新当前 trade。该逻辑不会检查 trade 是否反转,因为 Trade 在概念上 + 不支持反转。 - If an update sets the size attribute to 0, "closed" will be - set to true + 如果一次 update 将 size 设为 0,``closed`` 会被置为 true。 - Updates may be received twice for each order, once for the existing - size which has been closed (sell undoing a buy) and a second time for - the the opening part (sell reversing a buy) + 每个 order 可能收到两次 update:一次对应已关闭的现有 size(sell 抵消 + buy),另一次对应打开的新部分(sell 反转 buy)。 Args: - order: the order object which has (completely or partially) - generated this update - size (int): amount to update the order - if size has the same sign as the current trade a - position increase will happen - if size has the opposite sign as current op size a - reduction/close will happen - - price (float): always be positive to ensure consistency - value (float): (unused) cost incurred in new size/price op - Not used because the value is calculated for the - trade - commission (float): incurred commission in the new size/price op - pnl (float): (unused) generated by the executed part - Not used because the trade has an independent pnl + order: 完全或部分生成本次 update 的 order 对象。 + size (int): 用于更新 trade 的数量。如果 size 与当前 trade 同号, + 会增加 position;如果与当前 open size 异号,会减少或关闭 + position。 + price (float): 执行 price,必须为正以保持一致性。 + value (float): 未使用。新 size/price 操作产生的成本,trade 会自行 + 计算 value。 + commission (float): 新 size/price 操作产生的 commission。 + pnl (float): 未使用。执行部分产生的 pnl,trade 会独立计算 pnl。 + comminfo: 用于计算 value 和 profit/loss 的 CommissionInfo 对象。 + + Returns: + None: 直接更新当前 Trade 实例。 ''' if not size: - return # empty update, skip all other calculations + return # 空 update,跳过后续计算 - # Commission can only increase + # Commission 只能增加 self.commission += commission - # Update size and keep a reference for logic an calculations + # 更新 size,并保留旧 size 供逻辑和计算使用 oldsize = self.size - self.size += size # size will carry the opposite sign if reducing + self.size += size # 减仓时 size 会携带相反符号 - # Check if it has been currently opened + # 检查本次是否刚刚打开 self.justopened = bool(not oldsize and size) if self.justopened: @@ -266,16 +338,16 @@ def update(self, order, size, price, value, commission, pnl, self.dtopen = 0.0 if order.p.simulated else self.data.datetime[0] self.long = self.size > 0 - # Any size means the trade was opened + # 任何非零 size 都表示 trade 已打开 self.isopen = bool(self.size) - # Update current trade length + # 更新当前 trade 长度 self.barlen = len(self.data) - self.baropen - # record if the position was closed (set to null) + # 记录 position 是否被关闭(归零) self.isclosed = bool(oldsize and not self.size) - # record last bar for the trade + # 记录 trade 的最后一个 bar if self.isclosed: self.isopen = False self.barclose = len(self.data) @@ -286,13 +358,12 @@ def update(self, order, size, price, value, commission, pnl, self.status = self.Open if abs(self.size) > abs(oldsize): - # position increased (be it positive or negative) - # update the average price + # position 增加(无论正负),更新平均 price self.price = (oldsize * self.price + size * price) / self.size pnl = 0.0 else: # abs(self.size) < abs(oldsize) - # position reduced/closed + # position 减少或关闭 pnl = comminfo.profitandloss(-size, self.price, price) self.pnl += pnl @@ -300,7 +371,7 @@ def update(self, order, size, price, value, commission, pnl, self.value = comminfo.getvaluesize(self.size, self.price) - # Update the history if needed + # 如果需要,更新 history if self.historyon: dt0 = self.data.datetime[0] if not order.p.simulated else 0.0 histentry = TradeHistory( diff --git a/backtrader/tradingcal.py b/backtrader/tradingcal.py index c2c11b9f8..689caf39c 100644 --- a/backtrader/tradingcal.py +++ b/backtrader/tradingcal.py @@ -30,8 +30,8 @@ __all__ = ['TradingCalendarBase', 'TradingCalendar', 'PandasMarketCalendar'] -# Imprecission in the full time conversion to float would wrap over to next day -# if microseconds is 999999 as defined in time.max +# full time 转为 float 时有精度误差;如果 microseconds 使用 time.max 中定义的 +# 999999,可能会滚到下一天。 _time_max = time(hour=23, minute=59, second=59, microsecond=999990) @@ -45,113 +45,102 @@ class TradingCalendarBase(with_metaclass(MetaParams, object)): + '''trading calendar 的基类,用于定义交易日和 session 时间查询接口。''' + def _nextday(self, day): - ''' - Returns the next trading day (datetime/date instance) after ``day`` - (datetime/date instance) and the isocalendar components + '''返回 ``day`` 之后的下一个交易日及其 isocalendar 组件。 - The return value is a tuple with 2 components: (nextday, (y, w, d)) + Returns: + tuple: ``(nextday, (year, week, weekday))``。 ''' raise NotImplementedError def schedule(self, day): - ''' - Returns a tuple with the opening and closing times (``datetime.time``) - for the given ``date`` (``datetime/date`` instance) + '''返回给定交易日的开盘和收盘时间。 + + Returns: + tuple: ``(opening, closing)``,元素通常为 ``datetime.time`` 或 + ``datetime.datetime``。 ''' raise NotImplementedError def nextday(self, day): - ''' - Returns the next trading day (datetime/date instance) after ``day`` - (datetime/date instance) - ''' - return self._nextday(day)[0] # 1st ret elem is next day + '''返回 ``day`` 之后的下一个交易日。''' + return self._nextday(day)[0] # 第 1 个返回元素是 next day def nextday_week(self, day): - ''' - Returns the iso week number of the next trading day, given a ``day`` - (datetime/date) instance - ''' - self._nextday(day)[1][1] # 2 elem is isocal / 0 - y, 1 - wk, 2 - day + '''返回 ``day`` 之后下一个交易日所属的 ISO week number。''' + self._nextday(day)[1][1] # 第 2 个元素是 isocal / 0 - y, 1 - wk, 2 - day def last_weekday(self, day): - ''' - Returns ``True`` if the given ``day`` (datetime/date) instance is the - last trading day of this week - ''' - # Next day must be greater than day. If the week changes is enough for - # a week change even if the number is smaller (year change) + '''如果给定 ``day`` 是本周最后一个交易日,则返回 ``True``。''' + # next day 必须大于 day。如果 week 改变,即使周数变小(跨年),也足以说明换周 return day.isocalendar()[1] != self._nextday(day)[1][1] def last_monthday(self, day): - ''' - Returns ``True`` if the given ``day`` (datetime/date) instance is the - last trading day of this month - ''' - # Next day must be greater than day. If the week changes is enough for - # a week change even if the number is smaller (year change) + '''如果给定 ``day`` 是本月最后一个交易日,则返回 ``True``。''' + # next day 必须大于 day。月份变化即表示换月 return day.month != self._nextday(day)[0].month def last_yearday(self, day): - ''' - Returns ``True`` if the given ``day`` (datetime/date) instance is the - last trading day of this month - ''' - # Next day must be greater than day. If the week changes is enough for - # a week change even if the number is smaller (year change) + '''如果给定 ``day`` 是本年最后一个交易日,则返回 ``True``。''' + # next day 必须大于 day。年份变化即表示换年 return day.year != self._nextday(day)[0].year class TradingCalendar(TradingCalendarBase): - ''' - Wrapper of ``pandas_market_calendars`` for a trading calendar. The package - ``pandas_market_calendar`` must be installed + '''简单 trading calendar 实现。 - Params: + Args: - ``open`` (default ``time.min``) - Regular start of the session + 常规 session start。 - ``close`` (default ``time.max``) - Regular end of the session + 常规 session end。 - ``holidays`` (default ``[]``) - List of non-trading days (``datetime.datetime`` instances) + 非交易日列表(``datetime.datetime`` 实例)。 - ``earlydays`` (default ``[]``) - List of tuples determining the date and opening/closing times of days - which do not conform to the regular trading hours where each tuple has - (``datetime.datetime``, ``datetime.time``, ``datetime.time`` ) + 提前收盘/特殊交易时间的日期列表。每个 tuple 形如 + ``(datetime.datetime, datetime.time, datetime.time)``。 - ``offdays`` (default ``ISOWEEKEND``) - A list of weekdays in ISO format (Monday: 1 -> Sunday: 7) in which the - market doesn't trade. This is usually Saturday and Sunday and hence the - default + 市场不交易的 ISO weekday 列表(Monday: 1 -> Sunday: 7)。默认是周六和周日。 + + Returns: + TradingCalendar: 可按交易日查询 session 时间的 calendar。 + + --- + 交互示例: + >>> from datetime import datetime + >>> cal = TradingCalendar() + >>> cal.nextday(datetime(2024, 1, 5)).date() + datetime.date(2024, 1, 8) ''' params = ( ('open', time.min), ('close', _time_max), - ('holidays', []), # list of non trading days (date) - ('earlydays', []), # list of tuples (date, opentime, closetime) - ('offdays', ISOWEEKEND), # list of non trading (isoweekdays) + ('holidays', []), # 非交易日列表(date) + ('earlydays', []), # tuple 列表:(date, opentime, closetime) + ('offdays', ISOWEEKEND), # 非交易日列表(isoweekdays) ) def __init__(self): - self._earlydays = [x[0] for x in self.p.earlydays] # speed up searches + self._earlydays = [x[0] for x in self.p.earlydays] # 加速查找 def _nextday(self, day): - ''' - Returns the next trading day (datetime/date instance) after ``day`` - (datetime/date instance) and the isocalendar components + '''返回 ``day`` 之后的下一个交易日及其 isocalendar 组件。 - The return value is a tuple with 2 components: (nextday, (y, w, d)) + Returns: + tuple: ``(nextday, (year, week, weekday))``。 ''' while True: day += ONEDAY @@ -162,19 +151,19 @@ def _nextday(self, day): return day, isocal def schedule(self, day, tz=None): - ''' - Returns the opening and closing times for the given ``day``. If the - method is called, the assumption is that ``day`` is an actual trading - day + '''返回给定交易日的开盘和收盘时间。 - The return value is a tuple with 2 components: opentime, closetime + 调用该方法时,默认 ``day`` 是实际交易日。 + + Returns: + tuple: ``(opentime, closetime)``。 ''' while True: dt = day.date() try: i = self._earlydays.index(dt) o, c = self.p.earlydays[i][1:] - except ValueError: # not found + except ValueError: # 未找到 o, c = self.p.open, self.p.close closing = datetime.combine(dt, c) @@ -182,7 +171,7 @@ def schedule(self, day, tz=None): closing = tz.localize(closing).astimezone(UTC) closing = closing.replace(tzinfo=None) - if day > closing: # current time over eos + if day > closing: # 当前时间超过 eos day += ONEDAY continue @@ -195,26 +184,25 @@ def schedule(self, day, tz=None): class PandasMarketCalendar(TradingCalendarBase): - ''' - Wrapper of ``pandas_market_calendars`` for a trading calendar. The package - ``pandas_market_calendar`` must be installed + '''``pandas_market_calendars`` 的 trading calendar wrapper。 + + 需要安装 ``pandas_market_calendars`` 包。 - Params: + Args: - ``calendar`` (default ``None``) - The param ``calendar`` accepts the following: + ``calendar`` 参数接受: - - string: the name of one of the calendars supported, for example - `NYSE`. The wrapper will attempt to get a calendar instance + - string: 支持的 calendar 名称,例如 `NYSE`。wrapper 会尝试获取 calendar 实例。 - - calendar instance: as returned by ``get_calendar('NYSE')`` + - calendar instance: 例如 ``get_calendar('NYSE')`` 返回的对象。 - ``cachesize`` (default ``365``) - Number of days to cache in advance for lookup + 为查询提前缓存的天数。 - See also: + 参考: - https://github.com/rsheftel/pandas_market_calendars @@ -222,34 +210,33 @@ class PandasMarketCalendar(TradingCalendarBase): ''' params = ( - ('calendar', None), # A pandas_market_calendars instance or exch name - ('cachesize', 365), # Number of days to cache in advance + ('calendar', None), # pandas_market_calendars 实例或交易所名称 + ('cachesize', 365), # 提前缓存的天数 ) def __init__(self): self._calendar = self.p.calendar - if isinstance(self._calendar, string_types): # use passed mkt name + if isinstance(self._calendar, string_types): # 使用传入的 market name import pandas_market_calendars as mcal self._calendar = mcal.get_calendar(self._calendar) - import pandas as pd # guaranteed because of pandas_market_calendars + import pandas as pd # pandas_market_calendars 保证可用 self.dcache = pd.DatetimeIndex([0.0]) self.idcache = pd.DataFrame(index=pd.DatetimeIndex([0.0])) self.csize = timedelta(days=self.p.cachesize) def _nextday(self, day): - ''' - Returns the next trading day (datetime/date instance) after ``day`` - (datetime/date instance) and the isocalendar components + '''返回 ``day`` 之后的下一个交易日及其 isocalendar 组件。 - The return value is a tuple with 2 components: (nextday, (y, w, d)) + Returns: + tuple: ``(nextday, (year, week, weekday))``。 ''' day += ONEDAY while True: i = self.dcache.searchsorted(day) if i == len(self.dcache): - # keep a cache of 1 year to speed up searching + # 保留 1 年 cache 以加速查找 self.dcache = self._calendar.valid_days(day, day + self.csize) continue @@ -257,24 +244,24 @@ def _nextday(self, day): return d, d.isocalendar() def schedule(self, day, tz=None): - ''' - Returns the opening and closing times for the given ``day``. If the - method is called, the assumption is that ``day`` is an actual trading - day + '''返回给定交易日的开盘和收盘时间。 + + 调用该方法时,默认 ``day`` 是实际交易日。 - The return value is a tuple with 2 components: opentime, closetime + Returns: + tuple: ``(opentime, closetime)``。 ''' while True: i = self.idcache.index.searchsorted(day.date()) if i == len(self.idcache): - # keep a cache of 1 year to speed up searching + # 保留 1 年 cache 以加速查找 self.idcache = self._calendar.schedule(day, day + self.csize) continue st = (x.tz_localize(None) for x in self.idcache.iloc[i, 0:2]) - opening, closing = st # Get utc naive times - if day > closing: # passed time is over the sessionend - day += ONEDAY # wrap over to next day + opening, closing = st # 获取 utc naive times + if day > closing: # 传入时间已超过 sessionend + day += ONEDAY # 滚到下一天 continue return opening.to_pydatetime(), closing.to_pydatetime() diff --git a/backtrader/utils/autodict.py b/backtrader/utils/autodict.py index ff2b101ac..a5d5d0300 100644 --- a/backtrader/utils/autodict.py +++ b/backtrader/utils/autodict.py @@ -27,17 +27,33 @@ def Tree(): + '''创建可递归自动生成子节点的 ``defaultdict``。 + + Returns: + defaultdict: 默认值仍为 ``Tree`` 的嵌套字典。 + + --- + 交互示例: + >>> t = Tree() + >>> t['a']['b'] = 1 + >>> t['a']['b'] + 1 + ''' return defaultdict(Tree) class AutoDictList(dict): + '''缺失 key 自动创建 ``list`` 的 dict。''' + def __missing__(self, key): value = self[key] = list() return value class DotDict(dict): - # If the attribut is not found in the usual places try the dict itself + '''支持以属性方式读取 item 的 dict。''' + + # 常规属性查找失败后,尝试从 dict 自身读取 def __getattr__(self, key): if key.startswith('__'): return super(DotDict, self).__getattr__(key) @@ -45,15 +61,27 @@ def __getattr__(self, key): class AutoDict(dict): + '''缺失 key 自动创建嵌套 ``AutoDict`` 的 dict。 + + --- + 交互示例: + >>> d = AutoDict() + >>> d.account.cash = 100 + >>> d['account']['cash'] + 100 + ''' + _closed = False def _close(self): + '''关闭自动创建行为,并递归关闭子 AutoDict。''' self._closed = True for key, val in self.items(): if isinstance(val, (AutoDict, AutoOrderedDict)): val._close() def _open(self): + '''重新打开自动创建行为。''' self._closed = False def __missing__(self, key): @@ -78,15 +106,27 @@ def __setattr__(self, key, value): class AutoOrderedDict(OrderedDict): + '''保持插入顺序且缺失 key 自动创建嵌套 ``AutoOrderedDict`` 的 dict。 + + --- + 交互示例: + >>> d = AutoOrderedDict() + >>> d.stats.pnl = 12 + >>> d['stats']['pnl'] + 12 + ''' + _closed = False def _close(self): + '''关闭自动创建行为,并递归关闭子 AutoOrderedDict。''' self._closed = True for key, val in self.items(): if isinstance(val, (AutoDict, AutoOrderedDict)): val._close() def _open(self): + '''重新打开自动创建行为。''' self._closed = False def __missing__(self, key): @@ -110,7 +150,7 @@ def __setattr__(self, key, value): self[key] = value - # Define math operations + # 定义数学操作 def __iadd__(self, other): if type(self) != type(other): return type(other)() + other @@ -142,4 +182,5 @@ def __itruediv__(self, other): return self + other def lvalues(self): + '''返回 values 的 list 兼容视图。''' return py3lvalues(self) diff --git a/backtrader/utils/dateintern.py b/backtrader/utils/dateintern.py index a69977c50..aa524a4f1 100644 --- a/backtrader/utils/dateintern.py +++ b/backtrader/utils/dateintern.py @@ -38,54 +38,62 @@ DSTDIFF = DSTOFFSET - STDOFFSET -# To avoid rounding errors taking dates to next day +# 避免 rounding error 把日期推到下一天 TIME_MAX = datetime.time(23, 59, 59, 999990) -# To avoid rounding errors taking dates to next day +# 避免 rounding error 把日期推到下一天 TIME_MIN = datetime.time.min def tzparse(tz): - # If no object has been provided by the user and a timezone can be - # found via contractdtails, then try to get it from pytz, which may or - # may not be available. + '''解析 timezone 参数。 + + Args: + tz: ``None``、timezone 对象或 timezone 名称。 + + Returns: + timezone: 可用的 timezone/localizer 对象,或原值的 localizer 包装。 + ''' + # 如果用户未提供对象,但可通过 contract details 找到 timezone, + # 则尝试从 pytz 获取;pytz 可能可用也可能不可用。 tzstr = isinstance(tz, string_types) if tz is None or not tzstr: return Localizer(tz) try: - import pytz # keep the import very local + import pytz # 保持 import 非常局部 except ImportError: - return Localizer(tz) # nothing can be done + return Localizer(tz) # 无法进一步处理 tzs = tz - if tzs == 'CST': # usual alias + if tzs == 'CST': # 常见别名 tzs = 'CST6CDT' try: tz = pytz.timezone(tzs) except pytz.UnknownTimeZoneError: - return Localizer(tz) # nothing can be done + return Localizer(tz) # 无法进一步处理 return tz def Localizer(tz): + '''确保 timezone 对象拥有 ``localize`` 方法。''' import types def localize(self, dt): return dt.replace(tzinfo=self) if tz is not None and not hasattr(tz, 'localize'): - # patch the tz instance with a bound method + # 用 bound method patch tz 实例 tz.localize = types.MethodType(localize, tz) return tz -# A UTC class, same as the one in the Python Docs +# UTC 类,与 Python Docs 中的实现一致 class _UTC(datetime.tzinfo): - """UTC""" + """UTC timezone 实现。""" def utcoffset(self, dt): return ZERO @@ -101,6 +109,7 @@ def localize(self, dt): class _LocalTimezone(datetime.tzinfo): + '''本地 timezone 实现,用于根据系统 DST 规则计算 offset。''' def utcoffset(self, dt): if self._isdst(dt): @@ -124,7 +133,7 @@ def _isdst(self, dt): try: stamp = _time.mktime(tt) except (ValueError, OverflowError): - return False # Too far in the future, not relevant + return False # 距离未来太远,不相关 tt = _time.localtime(stamp) return tt.tm_isdst > 0 @@ -147,19 +156,25 @@ def localize(self, dt): def num2date(x, tz=None, naive=True): - # Same as matplotlib except if tz is None a naive datetime object - # will be returned. - """ - *x* is a float value which gives the number of days - (fraction part represents hours, minutes, seconds) since - 0001-01-01 00:00:00 UTC *plus* *one*. - The addition of one here is a historical artifact. Also, note - that the Gregorian calendar is assumed; this is not universal - practice. For details, see the module docstring. - Return value is a :class:`datetime` instance in timezone *tz* (default to - rcparams TZ value). - If *x* is a sequence, a sequence of :class:`datetime` objects will - be returned. + # 与 matplotlib 类似,但 tz 为 None 时返回 naive datetime object。 + """将 float 日期数转换为 ``datetime``。 + + ``x`` 是从 ``0001-01-01 00:00:00 UTC`` 加一天后开始计算的天数; + 小数部分表示 hours、minutes、seconds。这里额外加一天是历史遗留行为。 + 该函数假设使用 Gregorian calendar。 + + Args: + x: float 日期数。 + tz: 可选 timezone。 + naive: 指定 tz 时是否移除返回值上的 ``tzinfo``。 + + Returns: + datetime.datetime: 转换后的 datetime。 + + --- + 交互示例: + >>> num2date(date2num(datetime.datetime(2024, 1, 2, 3, 4, 5))) + datetime.datetime(2024, 1, 2, 3, 4, 5) """ ix = int(x) @@ -180,7 +195,7 @@ def num2date(x, tz=None, naive=True): if naive: dt = dt.replace(tzinfo=None) else: - # If not tz has been passed return a non-timezoned dt + # 未传入 tz 时返回不带 timezone 的 dt dt = datetime.datetime( dt.year, dt.month, dt.day, int(hour), int(minute), int(second), microsecond) @@ -192,18 +207,31 @@ def num2date(x, tz=None, naive=True): def num2dt(num, tz=None, naive=True): + '''将 numeric 日期转换为 ``datetime.date``。''' return num2date(num, tz=tz, naive=naive).date() def num2time(num, tz=None, naive=True): + '''将 numeric 日期转换为 ``datetime.time``。''' return num2date(num, tz=tz, naive=naive).time() def date2num(dt, tz=None): - """ - Convert :mod:`datetime` to the Gregorian date as UTC float days, - preserving hours, minutes, seconds and microseconds. Return value - is a :func:`float`. + """将 ``datetime`` 转换为 Gregorian UTC float days。 + + 会保留 hours、minutes、seconds 和 microseconds。 + + Args: + dt: ``datetime.datetime`` 或 ``datetime.date``。 + tz: 可选 timezone;传入时先 localize。 + + Returns: + float: 转换后的日期数。 + + --- + 交互示例: + >>> date2num(datetime.datetime(2024, 1, 1)) + 738886.0 """ if tz is not None: dt = tz.localize(dt) @@ -228,9 +256,18 @@ def date2num(dt, tz=None): def time2num(tm): - """ - Converts the hour/minute/second/microsecond part of tm (datetime.datetime - or time) to a num + """将 time 或 datetime 的日内部分转换为 numeric fraction。 + + Args: + tm: ``datetime.datetime`` 或 ``datetime.time``。 + + Returns: + float: 日内时间对应的一天内比例。 + + --- + 交互示例: + >>> time2num(datetime.time(12, 0)) + 0.5 """ num = (tm.hour / HOURS_PER_DAY + tm.minute / MINUTES_PER_DAY + diff --git a/backtrader/utils/flushfile.py b/backtrader/utils/flushfile.py index 6d0361456..0eef9dfc1 100644 --- a/backtrader/utils/flushfile.py +++ b/backtrader/utils/flushfile.py @@ -25,6 +25,11 @@ class flushfile(object): + '''自动 flush 的文件包装器,用于在写入后立即刷新输出。 + + Args: + - ``f``: 被包装的文件对象。 + ''' def __init__(self, f): self.f = f @@ -42,6 +47,10 @@ def flush(self): class StdOutDevNull(object): + '''临时吞掉 ``sys.stdout`` 输出的辅助类。 + + 创建实例时会保存当前 ``sys.stdout`` 并替换为自身;调用 ``stop`` 后恢复原输出。 + ''' def __init__(self): self.stdout = sys.stdout diff --git a/backtrader/utils/ordereddefaultdict.py b/backtrader/utils/ordereddefaultdict.py index a2335832f..10ebfad80 100644 --- a/backtrader/utils/ordereddefaultdict.py +++ b/backtrader/utils/ordereddefaultdict.py @@ -18,7 +18,7 @@ # along with this program. If not, see . # ############################################################################### -# From: http://stackoverflow.com/questions/4126348/how-do-i-rewrite-this-function-to-implement-ordereddict/4127426#4127426 +# 来源:http://stackoverflow.com/questions/4126348/how-do-i-rewrite-this-function-to-implement-ordereddict/4127426#4127426 ############################################################################### from __future__ import (absolute_import, division, print_function, unicode_literals) @@ -29,6 +29,23 @@ class OrderedDefaultdict(OrderedDict): + '''保持插入顺序的 ``defaultdict``。 + + Args: + *args: 第一个位置参数可为 default factory,其余参数传给 ``OrderedDict``。 + **kwargs: 传给 ``OrderedDict`` 的初始键值。 + + Returns: + OrderedDefaultdict: 读取缺失 key 时可自动创建默认值的有序字典。 + + --- + 交互示例: + >>> d = OrderedDefaultdict(list) + >>> d['orders'].append(1) + >>> d['orders'] + [1] + ''' + def __init__(self, *args, **kwargs): if not args: self.default_factory = None @@ -45,6 +62,6 @@ def __missing__(self, key): self[key] = default = self.default_factory() return default - def __reduce__(self): # optional, for pickle support + def __reduce__(self): # 可选:支持 pickle args = (self.default_factory,) if self.default_factory else () return self.__class__, args, None, None, iteritems(self) diff --git a/backtrader/utils/py3.py b/backtrader/utils/py3.py index 60946b685..7ff23cb2a 100644 --- a/backtrader/utils/py3.py +++ b/backtrader/utils/py3.py @@ -120,12 +120,11 @@ def items(d): return list(d.items()) import queue as queue -# This is from Armin Ronacher from Flash simplified later by six +# 来源:Armin Ronacher 的 Flask 实现,后来由 six 简化 def with_metaclass(meta, *bases): - """Create a base class with a metaclass.""" - # This requires a bit of explanation: the basic idea is to make a dummy - # metaclass for one level of class instantiation that replaces itself with - # the actual metaclass. + """创建带 metaclass 的基类。""" + # 基本思路:创建一个临时 dummy metaclass,用于一次 class 实例化, + # 然后用实际 metaclass 替换自身。 class metaclass(meta): def __new__(cls, name, this_bases, d): diff --git a/backtrader/writer.py b/backtrader/writer.py index 7a4cecb91..34220783c 100644 --- a/backtrader/writer.py +++ b/backtrader/writer.py @@ -26,10 +26,10 @@ import itertools import sys -try: # For new Python versions +try: # 新 Python 版本 collectionsAbc = collections.abc # collections.Iterable -> collections.abc.Iterable -except AttributeError: # For old Python versions - collectionsAbc = collections # Используем collections.Iterable +except AttributeError: # 旧 Python 版本 + collectionsAbc = collections # 使用 collections.Iterable import backtrader as bt from backtrader.utils.py3 import (map, with_metaclass, string_types, @@ -41,54 +41,42 @@ class WriterBase(with_metaclass(bt.MetaParams, object)): class WriterFile(WriterBase): - '''The system wide writer class. + '''系统级 writer 类,用于输出运行信息和可选 csv 数据流。 - It can be parametrized with: + Args: + - ``out`` (default: ``sys.stdout``): 要写入的输出流。 - - ``out`` (default: ``sys.stdout``): output stream to write to + 如果传入字符串,会将该参数内容作为文件名使用。 - If a string is passed a filename with the content of the parameter will - be used. + 如果希望在 multiprocess optimization 时使用 ``sys.stdout``,请保持为 + ``None``;子进程会自动初始化 ``sys.stdout``。 - If you wish to run with ``sys.stdout`` while doing multiprocess optimization, leave it as ``None``, which will - automatically initiate ``sys.stdout`` on the child processes. + - ``close_out`` (default: ``False``): 当 ``out`` 是 stream 时,writer 是否 + 需要显式关闭它。 - - ``close_out`` (default: ``False``) + - ``csv`` (default: ``False``): 是否在执行期间把 data feeds、strategies、 + observers 和 indicators 的 csv stream 写入输出流。 - If ``out`` is a stream whether it has to be explicitly closed by the - writer + 哪些对象实际进入 csv stream,可通过各对象的 ``csv`` 属性控制。默认情况下 + ``data feeds`` 和 ``observers`` 为 ``True``,``indicators`` 为 ``False``。 - - ``csv`` (default: ``False``) + - ``csv_filternan`` (default: ``True``): 是否从 csv stream 中清理 ``nan``, + 并替换为空字段。 - If a csv stream of the data feeds, strategies, observers and indicators - has to be written to the stream during execution + - ``csv_counter`` (default: ``True``): 是否保留并输出实际写出行数的计数器。 - Which objects actually go into the csv stream can be controlled with - the ``csv`` attribute of each object (defaults to ``True`` for ``data - feeds`` and ``observers`` / False for ``indicators``) - - - ``csv_filternan`` (default: ``True``) whether ``nan`` values have to be - purged out of the csv stream (replaced by an empty field) - - - ``csv_counter`` (default: ``True``) if the writer shall keep and print - out a counter of the lines actually output - - - ``indent`` (default: ``2``) indentation spaces for each level + - ``indent`` (default: ``2``): 每一层缩进使用的空格数。 - ``separators`` (default: ``['=', '-', '+', '*', '.', '~', '"', '^', - '#']``) - - Characters used for line separators across section/sub(sub)sections - - - ``seplen`` (default: ``79``) - - total length of a line separator including indentation + '#']``): section/subsection 分隔线使用的字符。 - - ``rounding`` (default: ``None``) + - ``seplen`` (default: ``79``): 分隔线总长度,包含缩进。 - Number of decimal places to round floats down to. With ``None`` no - rounding is performed + - ``rounding`` (default: ``None``): float 向下保留的小数位数;``None`` 表示 + 不做 rounding。 + Returns: + WriterFile: 可由 Cerebro 调用的 writer 实例。 ''' params = ( ('out', None), @@ -111,7 +99,7 @@ def __init__(self): self.values = list() def _start_output(self): - # open file if needed + # 按需打开文件 if not hasattr(self, 'out') or not self.out: if self.p.out is None: self.out = sys.stdout @@ -210,7 +198,7 @@ def writedict(self, dct, level=0, recurse=False): self.writelineseparator(level=level) self.writeline(kline) self.writedict(val, level=level + 1, recurse=True) - elif isinstance(val, (list, tuple, collectionsAbc.Iterable)): # Для разных версий Python будут вызываться разные функции + elif isinstance(val, (list, tuple, collectionsAbc.Iterable)): # 不同 Python 版本会调用不同实现 line = ', '.join(map(str, val)) self.writeline(kline + ' ' + line) else: @@ -230,5 +218,5 @@ def _start_output(self): def stop(self): super(WriterStringIO, self).stop() - # Leave the file positioned at the beginning + # 将文件位置留在开头 self.out.seek(0) diff --git a/samples/calendar-days/calendar-days.py b/samples/calendar-days/calendar-days.py index ab0df70a0..d21311d43 100644 --- a/samples/calendar-days/calendar-days.py +++ b/samples/calendar-days/calendar-days.py @@ -33,13 +33,13 @@ def runstrat(): args = parse_args() - # Create a cerebro entity + # 创建 cerebro 实体 cerebro = bt.Cerebro(stdstats=False) - # Add a strategy + # 添加 strategy cerebro.addstrategy(bt.Strategy) - # Get the dates from the args + # 从参数中读取日期 fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') @@ -57,21 +57,21 @@ def runstrat(): fill_price=args.fprice, fill_vol=args.fvol) - # Add the resample data instead of the original + # 添加处理后的 data,而不是原始 data cerebro.adddata(data) - # Add a simple moving average if requirested + # 如果需要,添加 Simple Moving Average if args.sma: cerebro.addindicator(btind.SMA, period=args.period) - # Add a writer with CSV + # 添加带 CSV 选项的 writer if args.writer: cerebro.addwriter(bt.WriterFile, csv=args.wrcsv) - # Run over everything + # 运行整个系统 cerebro.run() - # Plot if requested + # 如果需要则绘图 if args.plot: cerebro.plot(style='bar', numfigs=args.numfigs, volume=False) diff --git a/tests/fake_data/README.md b/tests/fake_data/README.md new file mode 100644 index 000000000..96bf45d1a --- /dev/null +++ b/tests/fake_data/README.md @@ -0,0 +1,20 @@ +# 假数据场景 + +该目录保存很小的本地假数据和可执行场景,用于在不依赖外部服务的情况下, +快速测试 backtrader 核心对象。 + +在仓库根目录运行: + +```bash +PYTHONPATH=. python3 tests/fake_data/scenarios.py +``` + +场景覆盖: + +- position 更新 +- order execution bits 和 pending clone notifications +- trade 生命周期更新 +- commission info 计算 +- sizer 和 broker commission lookup +- volume fillers +- 基于假 OHLCV 数据的小型 Cerebro 运行 diff --git a/tests/fake_data/scenarios.py b/tests/fake_data/scenarios.py new file mode 100644 index 000000000..f22deaccd --- /dev/null +++ b/tests/fake_data/scenarios.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python +from __future__ import absolute_import, division, print_function + +import datetime +import os + +import backtrader as bt + + +class FakeDateTimeLine(object): + def __getitem__(self, index): + return 0.0 + + def date(self): + return datetime.date(2020, 1, 1) + + +class FakeData(object): + _name = 'FAKE' + datetime = FakeDateTimeLine() + close = [100.0] + _tz = None + + def __len__(self): + return 0 + + def num2date(self, value, tz=None, naive=True): + return datetime.datetime(2020, 1, 1) + + +class FakeCommInfo(object): + def getvaluesize(self, size, price): + return abs(size) * price + + def profitandloss(self, size, price, newprice): + return size * (newprice - price) + + def getoperationcost(self, size, price): + return abs(size) * price + + def getcommission(self, size, price): + return abs(size) * price * 0.001 + + +def execute_order(position, order, size, price, partial): + pprice_orig = position.price + psize, pprice, opened, closed = position.update(size, price) + + comminfo = order.comminfo + closedvalue = comminfo.getoperationcost(closed, pprice_orig) + closedcomm = comminfo.getcommission(closed, price) + openedvalue = comminfo.getoperationcost(opened, price) + openedcomm = comminfo.getcommission(opened, price) + pnl = comminfo.profitandloss(-closed, pprice_orig, price) + margin = comminfo.getvaluesize(size, price) + + order.execute(order.data.datetime[0], + size, price, + closed, closedvalue, closedcomm, + opened, openedvalue, openedcomm, + margin, pnl, + psize, pprice) + + if partial: + order.partial() + else: + order.completed() + + +def scenario_position(): + pos = bt.Position(size=10, price=100.0) + assert pos.update(size=5, price=110.0) == (15, 103.33333333333333, 5, 0) + assert pos.update(size=-8, price=120.0) == (7, 103.33333333333333, 0, -8) + assert pos.update(size=-12, price=90.0) == (-5, 90.0, -5, -7) + return {'size': pos.size, 'price': pos.price} + + +def scenario_order_pending(): + position = bt.Position() + order = bt.BuyOrder(data=FakeData(), + size=100, price=1.0, + exectype=bt.Order.Market, + simulated=True) + order.addcomminfo(FakeCommInfo()) + + execute_order(position, order, 10, 1.0, True) + execute_order(position, order, 20, 1.1, True) + clone = order.clone() + pending = clone.executed.getpending() + assert [(bit.size, bit.price) for bit in pending] == [(10, 1.0), (20, 1.1)] + + execute_order(position, order, 30, 1.2, True) + execute_order(position, order, 40, 1.3, False) + clone = order.clone() + pending = clone.executed.getpending() + assert [(bit.size, bit.price) for bit in pending] == [(30, 1.2), (40, 1.3)] + assert order.status == bt.Order.Completed + return {'status': order.getstatusname(), 'pending': len(pending)} + + +def scenario_trade(): + trade = bt.Trade(data=FakeData(), historyon=True) + order = bt.BuyOrder(data=FakeData(), + size=0, price=1.0, + exectype=bt.Order.Market, + simulated=True) + comminfo = FakeCommInfo() + + trade.update(order=order, size=10, price=10.0, value=100.0, + commission=1.0, pnl=0.0, comminfo=comminfo) + assert trade.isopen and not trade.isclosed + + trade.update(order=order, size=-4, price=12.0, value=48.0, + commission=0.5, pnl=0.0, comminfo=comminfo) + assert trade.size == 6 + assert trade.pnl == 8.0 + + trade.update(order=order, size=-6, price=11.0, value=66.0, + commission=0.5, pnl=0.0, comminfo=comminfo) + assert trade.isclosed + assert trade.pnl == 14.0 + assert len(trade.history) == 3 + return {'pnl': trade.pnl, 'pnlcomm': trade.pnlcomm} + + +def scenario_comminfo(): + stock = bt.CommissionInfo(commission=0.01) + futures = bt.CommissionInfo(commission=2.0, margin=1000.0, mult=10.0) + pos = bt.Position(size=3, price=100.0) + + assert stock.getoperationcost(size=3, price=100.0) == 300.0 + assert stock.getcommission(size=3, price=100.0) == 3.0 + assert stock.profitandloss(pos.size, pos.price, 105.0) == 15.0 + + assert futures.getoperationcost(size=3, price=100.0) == 3000.0 + assert futures.getcommission(size=3, price=100.0) == 6.0 + assert futures.profitandloss(pos.size, pos.price, 105.0) == 150.0 + return {'stock_commission': 3.0, 'futures_commission': 6.0} + + +class FakeBrokerForSizer(object): + def __init__(self): + self.comminfo = bt.CommInfoBase(commission=0.0) + + def getcommissioninfo(self, data): + return self.comminfo + + def getcash(self): + return 1234.0 + + +class TenSizer(bt.Sizer): + def _getsizing(self, comminfo, cash, data, isbuy): + assert cash == 1234.0 + return 10 if isbuy else 5 + + +def scenario_sizer_and_broker(): + class Data(object): + _name = 'FAKE' + + broker = bt.BrokerBase() + broker.setcommission(commission=0.5, name='FAKE') + assert broker.getcommissioninfo(Data()).p.commission == 0.5 + + sizer = TenSizer() + sizer.set(strategy='strategy', broker=FakeBrokerForSizer()) + assert sizer.getsizing(data=Data(), isbuy=True) == 10 + assert sizer.getsizing(data=Data(), isbuy=False) == 5 + return {'buy_size': 10, 'sell_size': 5} + + +class FakeLine(object): + def __init__(self, value): + self.value = value + + def __getitem__(self, ago): + return self.value + + +def scenario_fillers(): + class Data(object): + high = FakeLine(101.0) + low = FakeLine(100.0) + volume = FakeLine(100) + + class Executed(object): + remsize = 80 + + class Order(object): + data = Data() + executed = Executed() + + assert bt.broker.fillers.FixedSize(size=10)(Order(), 100.0, 0) == 10 + assert bt.broker.fillers.FixedBarPerc(perc=50.0)(Order(), 100.0, 0) == 50.0 + assert bt.broker.fillers.BarPointPerc(minmov=0.5, perc=50.0)( + Order(), 100.5, 0) == 16.0 + return {'fixed': 10, 'bar_perc': 50.0, 'point_perc': 16.0} + + +class BuyThenCloseStrategy(bt.Strategy): + def __init__(self): + self.orders_seen = 0 + self.trades_seen = 0 + + def next(self): + if len(self) == 1: + self.buy(size=1) + elif len(self) == 4 and self.position: + self.close() + + def notify_order(self, order): + if order.status in [order.Completed, order.Partial]: + self.orders_seen += 1 + + def notify_trade(self, trade): + if trade.isclosed: + self.trades_seen += 1 + + +def scenario_cerebro_run(): + data_path = os.path.join(os.path.dirname(__file__), 'tiny-ohlcv.csv') + data = bt.feeds.GenericCSVData( + dataname=data_path, + dtformat='%Y-%m-%d', + datetime=0, + open=1, + high=2, + low=3, + close=4, + volume=5, + openinterest=6, + headers=True) + + cerebro = bt.Cerebro(stdstats=False) + cerebro.broker.setcash(10000.0) + cerebro.adddata(data) + cerebro.addstrategy(BuyThenCloseStrategy) + cerebro.addsizer(bt.sizers.FixedSize, stake=1) + cerebro.addanalyzer(bt.analyzers.PeriodStats, _name='periodstats') + cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades') + cerebro.addobserver(bt.observers.Broker) + cerebro.addobserver(bt.observers.FundValue) + + strategies = cerebro.run() + strategy = strategies[0] + periodstats = strategy.analyzers.periodstats.get_analysis() + trades = strategy.analyzers.trades.get_analysis() + + assert strategy.orders_seen >= 2 + assert strategy.trades_seen == 1 + assert 'average' in periodstats + assert trades.total.closed == 1 + return { + 'final_value': round(cerebro.broker.getvalue(), 2), + 'closed_trades': trades.total.closed, + } + + +def run_all(): + scenarios = [ + ('position', scenario_position), + ('order_pending', scenario_order_pending), + ('trade', scenario_trade), + ('comminfo', scenario_comminfo), + ('sizer_and_broker', scenario_sizer_and_broker), + ('fillers', scenario_fillers), + ('cerebro_run', scenario_cerebro_run), + ] + + results = {} + for name, func in scenarios: + results[name] = func() + + for name in sorted(results): + print('{}: {}'.format(name, results[name])) + print('fake data scenarios ok') + + +if __name__ == '__main__': + run_all() diff --git a/tests/fake_data/tiny-ohlcv.csv b/tests/fake_data/tiny-ohlcv.csv new file mode 100644 index 000000000..93021d2f7 --- /dev/null +++ b/tests/fake_data/tiny-ohlcv.csv @@ -0,0 +1,7 @@ +date,open,high,low,close,volume,openinterest +2020-01-01,100.0,101.0,99.0,100.0,1000,0 +2020-01-02,100.0,103.0,99.5,102.0,1100,0 +2020-01-03,102.0,104.0,101.0,103.0,1200,0 +2020-01-06,103.0,106.0,102.0,105.0,1300,0 +2020-01-07,105.0,106.0,100.0,101.0,1400,0 +2020-01-08,101.0,102.0,98.0,99.0,1500,0 From 99e5ae7893c474369e7a3ea4d7eb53db322de329 Mon Sep 17 00:00:00 2001 From: liu Date: Sat, 27 Jun 2026 09:54:25 +0800 Subject: [PATCH 2/2] Translate sample and test comments --- .../analyzer-annualreturn.py | 37 +++++++------- samples/bracket/bracket.py | 8 +-- samples/btfd/btfd.py | 12 ++--- samples/calmar/calmar-test.py | 8 +-- samples/cheat-on-open/cheat-on-open.py | 8 +-- .../commission-schemes/commission-schemes.py | 26 +++++----- samples/credit-interest/credit-interest.py | 4 +- samples/data-bid-ask/bidask.py | 6 +-- samples/data-filler/data-filler.py | 26 +++++----- samples/data-filler/relativevolume.py | 6 +-- .../data-multitimeframe.py | 16 +++--- samples/data-pandas/data-pandas-optix.py | 16 +++--- samples/data-pandas/data-pandas.py | 14 +++--- samples/data-replay/data-replay.py | 14 +++--- samples/data-resample/data-resample.py | 20 ++++---- samples/future-spot/future-spot.py | 2 +- samples/gold-vs-sp500/gold-vs-sp500.py | 8 +-- samples/ibtest/ibtest.py | 14 +++--- samples/kselrsi/ksignal.py | 2 +- samples/lineplotter/lineplotter.py | 4 +- samples/lrsi/lrsi-test.py | 8 +-- samples/macd-settings/macd-settings.py | 50 ++++++++----------- samples/multi-copy/multi-copy.py | 20 ++++---- samples/multi-example/mult-values.py | 8 +-- .../multidata-strategy-unaligned.py | 45 ++++++++--------- .../multidata-strategy/multidata-strategy.py | 45 ++++++++--------- samples/multitrades/multitrades.py | 39 +++++++-------- samples/oandatest/oandatest.py | 16 +++--- .../observer-benchmark/observer-benchmark.py | 4 +- .../observers/observers-default-drawdown.py | 12 ++--- samples/observers/observers-orderobserver.py | 16 +++--- samples/observers/orderobserver.py | 5 +- samples/oco/oco.py | 8 +-- samples/optimization/optimization.py | 20 ++++---- samples/order-close/close-daily.py | 23 ++++----- samples/order-execution/order-execution.py | 20 ++++---- samples/order-history/order-history.py | 8 +-- samples/order_target/order_target.py | 22 +++----- samples/partial-plot/partial-plot.py | 10 ++-- .../pinkfish-challenge/pinkfish-challenge.py | 47 ++++++++--------- samples/plot-same-axis/plot-same-axis.py | 18 +++---- samples/psar/psar-intraday.py | 8 +-- samples/psar/psar.py | 8 +-- samples/pyfolio2/pyfoliotest.py | 8 +-- samples/pyfoliotest/pyfoliotest.py | 2 +- samples/relative-volume/relative-volume.py | 22 ++++---- samples/relative-volume/relvolbybar.py | 20 ++++---- samples/renko/renko.py | 8 +-- .../resample-tickdata/resample-tickdata.py | 16 +++--- samples/rollover/rollover.py | 10 ++-- .../sharpe-timereturn/sharpe-timereturn.py | 20 ++++---- samples/signals-strategy/signals-strategy.py | 4 +- samples/sizertest/sizertest.py | 4 +- samples/slippage/slippage.py | 4 +- samples/sratio/sratio.py | 2 +- samples/stop-trading/stop-loss-approaches.py | 30 +++++------ samples/stoptrail/trail.py | 8 +-- samples/talib/tablibsartest.py | 2 +- samples/talib/talibtest.py | 2 +- samples/timers/scheduled-min.py | 8 +-- samples/timers/scheduled.py | 8 +-- samples/tradingcalendar/tcal-intra.py | 8 +-- samples/tradingcalendar/tcal.py | 8 +-- samples/vctest/vctest.py | 12 ++--- samples/volumefilling/volumefilling.py | 2 +- samples/vwr/vwr.py | 18 +++---- samples/weekdays-filler/weekdaysfiller.py | 16 +++--- samples/writer-test/writer-test.py | 35 +++++++------ samples/yahoo-test/yahoo-test.py | 16 +++--- tests/test_analyzer-sqn.py | 8 +-- tests/test_analyzer-timereturn.py | 8 +-- tests/test_metaclass.py | 14 ++---- tests/test_order.py | 6 +-- tests/test_strategy_optimized.py | 4 +- tests/test_strategy_unoptimized.py | 6 +-- tests/testcommon.py | 9 ++-- 76 files changed, 513 insertions(+), 546 deletions(-) diff --git a/samples/analyzer-annualreturn/analyzer-annualreturn.py b/samples/analyzer-annualreturn/analyzer-annualreturn.py index 9bcb5224e..1e9af5877 100644 --- a/samples/analyzer-annualreturn/analyzer-annualreturn.py +++ b/samples/analyzer-annualreturn/analyzer-annualreturn.py @@ -24,7 +24,7 @@ import argparse import datetime -# The above could be sent to an independent module +# 上方内容可放到独立 module 中 import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind @@ -33,10 +33,9 @@ class LongShortStrategy(bt.Strategy): - '''This strategy buys/sells upong the close price crossing - upwards/downwards a Simple Moving Average. + '''根据 close price 与 Simple Moving Average 的上下交叉执行买卖。 - It can be a long-only strategy by setting the param "onlylong" to True + 将 ``onlylong`` 参数设为 ``True`` 时,可作为 long-only strategy 使用。 ''' params = dict( period=15, @@ -59,18 +58,18 @@ def log(self, txt, dt=None): print('%s, %s' % (dt.isoformat(), txt)) def __init__(self): - # To control operation entries + # 用于控制操作入口 self.orderid = None - # Create SMA on 2nd data + # 在第 2 个 data 上创建 SMA sma = btind.MovAv.SMA(self.data, period=self.p.period) - # Create a CrossOver Signal from close an moving average + # 从 close 和 moving average 创建 CrossOver Signal self.signal = btind.CrossOver(self.data.close, sma) self.signal.csv = self.p.csvcross def next(self): if self.orderid: - return # if an order is active, no new orders are allowed + return # 如果有 active order,则不允许新 orders if self.signal > 0.0: # cross upwards if self.position: @@ -105,7 +104,7 @@ def notify_order(self, order): self.log('%s ,' % order.Status[order.status]) pass # Simply log - # Allow new orders + # 允许新 orders self.orderid = None def notify_trade(self, trade): @@ -120,33 +119,33 @@ def notify_trade(self, trade): def runstrategy(): args = parse_args() - # Create a cerebro + # 创建 cerebro cerebro = bt.Cerebro() - # Get the dates from the args + # 从 args 获取日期 fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') - # Create the 1st data + # 创建第 1 个 data data = btfeeds.BacktraderCSVData( dataname=args.data, fromdate=fromdate, todate=todate) - # Add the 1st data to cerebro + # 添加第 1 个 data 到 cerebro cerebro.adddata(data) - # Add the strategy + # 添加 strategy cerebro.addstrategy(LongShortStrategy, period=args.period, onlylong=args.onlylong, csvcross=args.csvcross, stake=args.stake) - # Add the commission - only stocks like a for each operation + # 添加 commission;仅针对类似 stock 的每次操作 cerebro.broker.setcash(args.cash) - # Add the commission - only stocks like a for each operation + # 添加 commission;仅针对类似 stock 的每次操作 cerebro.broker.setcommission(commission=args.comm, mult=args.mult, margin=args.margin) @@ -157,7 +156,7 @@ def runstrategy(): months=bt.TimeFrame.Months, years=bt.TimeFrame.Years) - # Add the Analyzers + # 添加 Analyzers cerebro.addanalyzer(SQN) if args.legacyannual: cerebro.addanalyzer(AnnualReturn) @@ -170,10 +169,10 @@ def runstrategy(): cerebro.addwriter(bt.WriterFile, csv=args.writercsv, rounding=4) - # And run it + # 然后运行 cerebro.run() - # Plot if requested + # 按需绘图 if args.plot: cerebro.plot(numfigs=args.numfigs, volume=False, zdown=False) diff --git a/samples/bracket/bracket.py b/samples/bracket/bracket.py index 3df8326ca..a487db010 100644 --- a/samples/bracket/bracket.py +++ b/samples/bracket/bracket.py @@ -131,7 +131,7 @@ def runstrat(args=None): # Data feed kwargs kwargs = dict() - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -151,10 +151,10 @@ def runstrat(args=None): # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) - # Execute + # 执行 cerebro.run(**eval('dict(' + args.cerebro + ')')) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**eval('dict(' + args.plot + ')')) @@ -169,7 +169,7 @@ def parse_args(pargs=None): parser.add_argument('--data0', default='../../datas/2005-2006-day-001.txt', required=False, help='Data to read in') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/btfd/btfd.py b/samples/btfd/btfd.py index 6cf233d48..bf24f4fa3 100644 --- a/samples/btfd/btfd.py +++ b/samples/btfd/btfd.py @@ -32,7 +32,7 @@ class ValueUnlever(bt.observers.Value): - '''Extension of regular Value observer to add leveraged view''' + '''常规 Value observer 的扩展,用于增加 leveraged view。''' lines = ('value_lever', 'asset') params = (('assetstart', 100000.0), ('lever', True),) @@ -147,7 +147,7 @@ def runstrat(args=None): # Data feed kwargs kwargs = dict() - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): kwargs[d] = datetime.datetime.strptime(a, dtfmt + tmfmt * ('T' in a)) @@ -164,19 +164,19 @@ def runstrat(args=None): # Broker cerebro.broker = bt.brokers.BackBroker(**eval('dict(' + args.broker + ')')) - # Add a commission + # 添加 commission cerebro.broker.setcommission(**eval('dict(' + args.comminfo + ')')) # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) - # Add specific observer + # 添加特定 observer cerebro.addobserver(ValueUnlever, **eval('dict(' + args.valobserver + ')')) - # Execute + # 执行 cerebro.run(**eval('dict(' + args.cerebro + ')')) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**eval('dict(' + args.plot + ')')) diff --git a/samples/calmar/calmar-test.py b/samples/calmar/calmar-test.py index 02ceb7257..f72de8bc8 100644 --- a/samples/calmar/calmar-test.py +++ b/samples/calmar/calmar-test.py @@ -48,7 +48,7 @@ def runstrat(args=None): # Data feed kwargs kwargs = dict() - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -69,14 +69,14 @@ def runstrat(args=None): # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) - # Execute + # 执行 st0 = cerebro.run(**eval('dict(' + args.cerebro + ')'))[0] i = 1 for k, v in st0.analyzers.calmar.get_analysis().items(): print(i, ': '.join((str(k), str(v)))) i += 1 - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**eval('dict(' + args.plot + ')')) @@ -91,7 +91,7 @@ def parse_args(pargs=None): parser.add_argument('--data0', default='../../datas/orcl-1995-2014.txt', required=False, help='Data to read in') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/cheat-on-open/cheat-on-open.py b/samples/cheat-on-open/cheat-on-open.py index d0b658370..582e81914 100644 --- a/samples/cheat-on-open/cheat-on-open.py +++ b/samples/cheat-on-open/cheat-on-open.py @@ -86,7 +86,7 @@ def runstrat(args=None): # Data feed kwargs kwargs = dict() - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -106,10 +106,10 @@ def runstrat(args=None): # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) - # Execute + # 执行 cerebro.run(**eval('dict(' + args.cerebro + ')')) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**eval('dict(' + args.plot + ')')) @@ -124,7 +124,7 @@ def parse_args(pargs=None): parser.add_argument('--data0', default='../../datas/2005-2006-day-001.txt', required=False, help='Data to read in') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/commission-schemes/commission-schemes.py b/samples/commission-schemes/commission-schemes.py index 36a420006..4013ec061 100644 --- a/samples/commission-schemes/commission-schemes.py +++ b/samples/commission-schemes/commission-schemes.py @@ -42,11 +42,11 @@ def log(self, txt, dt=None): def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: - # Buy/Sell order submitted/accepted to/by broker - Nothing to do + # Buy/Sell order 已提交/被 broker 接受,无需处理 return - # Check if an order has been completed - # Attention: broker could reject order if not enougth cash + # 检查 order 是否已完成 + # 注意:现金不足时 broker 可能拒绝 order if order.status in [order.Completed, order.Canceled, order.Margin]: if order.isbuy(): self.log( @@ -67,7 +67,7 @@ def notify_trade(self, trade): def __init__(self): sma = btind.SMA(self.data, period=self.p.period) - # > 0 crossing up / < 0 crossing down + # > 0 向上交叉 / < 0 向下交叉 self.buysell_sig = btind.CrossOver(self.data, sma) def next(self): @@ -83,26 +83,26 @@ def next(self): def runstrategy(): args = parse_args() - # Create a cerebro + # 创建 cerebro cerebro = bt.Cerebro() - # Get the dates from the args + # 从 args 获取日期 fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') - # Create the 1st data + # 创建第 1 个 data data = btfeeds.BacktraderCSVData( dataname=args.data, fromdate=fromdate, todate=todate) - # Add the 1st data to cerebro + # 添加第 1 个 data 到 cerebro cerebro.adddata(data) - # Add a strategy + # 添加 strategy cerebro.addstrategy(SMACrossOver, period=args.period, stake=args.stake) - # Add the commission - only stocks like a for each operation + # 添加 commission;仅针对类似 stock 的每次操作 cerebro.broker.setcash(args.cash) commtypes = dict( @@ -110,7 +110,7 @@ def runstrategy(): perc=bt.CommInfoBase.COMM_PERC, fixed=bt.CommInfoBase.COMM_FIXED) - # Add the commission - only stocks like a for each operation + # 添加 commission;仅针对类似 stock 的每次操作 cerebro.broker.setcommission(commission=args.comm, mult=args.mult, margin=args.margin, @@ -118,10 +118,10 @@ def runstrategy(): commtype=commtypes[args.commtype], stocklike=args.stocklike) - # And run it + # 然后运行 cerebro.run() - # Plot if requested + # 按需绘图 if args.plot: cerebro.plot(numfigs=args.numfigs, volume=False) diff --git a/samples/credit-interest/credit-interest.py b/samples/credit-interest/credit-interest.py index b4e005c07..3ca9b837f 100644 --- a/samples/credit-interest/credit-interest.py +++ b/samples/credit-interest/credit-interest.py @@ -77,7 +77,7 @@ def runstrat(args=None): todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') dkwargs['todate'] = todate - # if dataset is None, args.data has been given + # 如果 dataset 为 None,说明已传入 args.data data = bt.feeds.BacktraderCSVData(dataname=args.data, **dkwargs) cerebro.adddata(data) @@ -181,7 +181,7 @@ def parse_args(pargs=None): default=10, type=int, help=('Stake to apply')) - # Plot options + # 绘图选项 parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' diff --git a/samples/data-bid-ask/bidask.py b/samples/data-bid-ask/bidask.py index 04d790bde..8ac0a82e6 100644 --- a/samples/data-bid-ask/bidask.py +++ b/samples/data-bid-ask/bidask.py @@ -85,11 +85,11 @@ def parse_args(): def runstrategy(): args = parse_args() - cerebro = bt.Cerebro() # Create a cerebro + cerebro = bt.Cerebro() # 创建 cerebro data = BidAskCSV(dataname=args.data, dtformat=args.dtformat) - cerebro.adddata(data) # Add the 1st data to cerebro - # Add the strategy to cerebro + cerebro.adddata(data) # 添加第 1 个 data 到 cerebro + # 添加 strategy to cerebro cerebro.addstrategy(St, sma=args.sma, period=args.period) cerebro.run() diff --git a/samples/data-filler/data-filler.py b/samples/data-filler/data-filler.py index eaafb7648..ea217a3e8 100644 --- a/samples/data-filler/data-filler.py +++ b/samples/data-filler/data-filler.py @@ -25,7 +25,7 @@ import datetime import math -# The above could be sent to an independent module +# 上方内容可放到独立 module 中 import backtrader as bt import backtrader.feeds as btfeeds import backtrader.utils.flushfile @@ -37,19 +37,19 @@ def runstrategy(): args = parse_args() - # Create a cerebro + # 创建 cerebro cerebro = bt.Cerebro() - # Get the dates from the args + # 从 args 获取日期 fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') - # Get the session times to pass them to the indicator - # datetime.time has no strptime ... + # 获取 session 时间并传给 indicator + # datetime.time 没有 strptime dtstart = datetime.datetime.strptime(args.tstart, '%H:%M') dtend = datetime.datetime.strptime(args.tend, '%H:%M') - # Create the 1st data + # 创建第 1 个 data data = btfeeds.BacktraderCSVData( dataname=args.data, fromdate=fromdate, @@ -66,28 +66,28 @@ def runstrategy(): if args.filler: data.addfilter(btfilters.SessionFiller, fill_vol=args.fvol) - # Add the data to cerebro + # 添加 data 到 cerebro cerebro.adddata(data) if args.relvol: - # Calculate backward period - tend tstart are in same day - # + 1 to include last moment of the interval dstart <-> dtend + # 计算向后 period;tend 和 tstart 位于同一天 + # +1 用于包含 dstart <-> dtend 区间最后一刻 td = ((dtend - dtstart).seconds // 60) + 1 cerebro.addindicator(RelativeVolume, period=td, volisnan=math.isnan(args.fvol)) - # Add an empty strategy + # 添加空 strategy cerebro.addstrategy(bt.Strategy) - # Add a writer with CSV + # 添加带 CSV 的 writer if args.writer: cerebro.addwriter(bt.WriterFile, csv=args.wrcsv) - # And run it - no trading - disable stdstats + # 然后运行 - no trading - disable stdstats cerebro.run(stdstats=False) - # Plot if requested + # 按需绘图 if args.plot: cerebro.plot(numfigs=args.numfigs, volume=True) diff --git a/samples/data-filler/relativevolume.py b/samples/data-filler/relativevolume.py index 52fba897c..daa3051eb 100644 --- a/samples/data-filler/relativevolume.py +++ b/samples/data-filler/relativevolume.py @@ -37,11 +37,11 @@ class RelativeVolume(bt.Indicator): def __init__(self): if self.p.volisnan: - # if missing volume will be NaN, do a simple division - # the end result for missing volumes will also be NaN + # 如果缺失 volume 会得到 NaN,此处做简单除法 + # 缺失 volume 的最终结果也会是 NaN relvol = self.data.volume(-self.p.period) / self.data.volume else: - # Else do a controlled Div with a built-in function + # 否则使用内置函数做受控除法 relvol = bt.DivByZero( self.data.volume(-self.p.period), self.data.volume, diff --git a/samples/data-multitimeframe/data-multitimeframe.py b/samples/data-multitimeframe/data-multitimeframe.py index 22feb6511..63d0ef137 100644 --- a/samples/data-multitimeframe/data-multitimeframe.py +++ b/samples/data-multitimeframe/data-multitimeframe.py @@ -93,10 +93,10 @@ def next(self): def runstrat(): args = parse_args() - # Create a cerebro entity + # 创建 cerebro 实体 cerebro = bt.Cerebro() - # Add a strategy + # 添加 strategy if not args.indicators: cerebro.addstrategy(bt.Strategy) else: @@ -108,7 +108,7 @@ def runstrat(): onlydaily=args.onlydaily, ) - # Load the Data + # 加载 Data datapath = args.dataname or '../../datas/2006-day-001.txt' data = btfeeds.BacktraderCSVData( dataname=datapath) @@ -118,8 +118,8 @@ def runstrat(): weekly=bt.TimeFrame.Weeks, monthly=bt.TimeFrame.Months) - # Handy dictionary for the argument timeframe conversion - # Resample the data + # 用于参数 timeframe 转换的便捷字典 + # resample data if args.noresample: datapath = args.dataname2 or '../../datas/2006-week-001.txt' data2 = btfeeds.BacktraderCSVData( @@ -154,19 +154,19 @@ def runstrat(): elif args.timeframe == 'monthly': data2.addfilter(ResamplerMonthly) - # First add the original data - smaller timeframe + # 先添加原始 data,即较小 timeframe cerebro.adddata(data) # And then the large timeframe cerebro.adddata(data2) - # Run over everything + # 运行全部流程 cerebro.run(runonce=not args.runnext, preload=not args.nopreload, oldsync=args.oldsync, stdstats=False) - # Plot the result + # 绘制结果 if args.plot: cerebro.plot(style='bar') diff --git a/samples/data-pandas/data-pandas-optix.py b/samples/data-pandas/data-pandas-optix.py index 264187ec9..f37b1762d 100644 --- a/samples/data-pandas/data-pandas-optix.py +++ b/samples/data-pandas/data-pandas-optix.py @@ -37,7 +37,7 @@ class PandasDataOptix(btfeeds.PandasData): ('optix_opt', -1)) if False: - # No longer needed with version 1.9.62.122 + # 1.9.62.122 版本后不再需要 datafields = btfeeds.PandasData.datafields + ( ['optix_close', 'optix_pess', 'optix_opt']) @@ -55,16 +55,16 @@ def next(self): def runstrat(): args = parse_args() - # Create a cerebro entity + # 创建 cerebro 实体 cerebro = bt.Cerebro(stdstats=False) - # Add a strategy + # 添加 strategy cerebro.addstrategy(StrategyOptix) - # Get a pandas dataframe + # 获取 pandas dataframe datapath = ('../../datas/2006-day-001-optix.txt') - # Simulate the header row isn't there if noheaders requested + # 如果请求 noheaders,则模拟不存在 header row skiprows = 1 if args.noheaders else 0 header = None if args.noheaders else 0 @@ -79,15 +79,15 @@ def runstrat(): print(dataframe) print('--------------------------------------------------') - # Pass it to the backtrader datafeed and add it to the cerebro + # 传给 backtrader datafeed 并添加到 cerebro data = PandasDataOptix(dataname=dataframe) cerebro.adddata(data) - # Run over everything + # 运行全部流程 cerebro.run() - # Plot the result + # 绘制结果 if not args.noplot: cerebro.plot(style='bar') diff --git a/samples/data-pandas/data-pandas.py b/samples/data-pandas/data-pandas.py index cc112a1a8..516d6028e 100644 --- a/samples/data-pandas/data-pandas.py +++ b/samples/data-pandas/data-pandas.py @@ -32,16 +32,16 @@ def runstrat(): args = parse_args() - # Create a cerebro entity + # 创建 cerebro 实体 cerebro = bt.Cerebro(stdstats=False) - # Add a strategy + # 添加 strategy cerebro.addstrategy(bt.Strategy) - # Get a pandas dataframe + # 获取 pandas dataframe datapath = ('../../datas/2006-day-001.txt') - # Simulate the header row isn't there if noheaders requested + # 如果请求 noheaders,则模拟不存在 header row skiprows = 1 if args.noheaders else 0 header = None if args.noheaders else 0 @@ -59,7 +59,7 @@ def runstrat(): print(dataframe) print('--------------------------------------------------') - # Pass it to the backtrader datafeed and add it to the cerebro + # 传给 backtrader datafeed 并添加到 cerebro data = bt.feeds.PandasData(dataname=dataframe, # datetime='Date', nocase=True, @@ -67,10 +67,10 @@ def runstrat(): cerebro.adddata(data) - # Run over everything + # 运行全部流程 cerebro.run() - # Plot the result + # 绘制结果 cerebro.plot(style='bar') diff --git a/samples/data-replay/data-replay.py b/samples/data-replay/data-replay.py index 685ba64ea..a887a6436 100644 --- a/samples/data-replay/data-replay.py +++ b/samples/data-replay/data-replay.py @@ -52,7 +52,7 @@ def next(self): def runstrat(): args = parse_args() - # Create a cerebro entity + # 创建 cerebro 实体 cerebro = bt.Cerebro(stdstats=False) cerebro.addstrategy( @@ -61,7 +61,7 @@ def runstrat(): period=args.period, ) - # Load the Data + # 加载 Data datapath = args.dataname or '../../datas//2006-day-001.txt' data = btfeeds.BacktraderCSVData( dataname=datapath) @@ -71,8 +71,8 @@ def runstrat(): weekly=bt.TimeFrame.Weeks, monthly=bt.TimeFrame.Months) - # Handy dictionary for the argument timeframe conversion - # Resample the data + # 用于参数 timeframe 转换的便捷字典 + # resample data if args.oldrp: data = bt.DataReplayer( dataname=data, @@ -83,13 +83,13 @@ def runstrat(): timeframe=tframes[args.timeframe], compression=args.compression) - # First add the original data - smaller timeframe + # 先添加原始 data,即较小 timeframe cerebro.adddata(data) - # Run over everything + # 运行全部流程 cerebro.run(preload=False) - # Plot the result + # 绘制结果 cerebro.plot(style='bar') diff --git a/samples/data-resample/data-resample.py b/samples/data-resample/data-resample.py index 228a8b69f..1863b4a67 100644 --- a/samples/data-resample/data-resample.py +++ b/samples/data-resample/data-resample.py @@ -30,44 +30,44 @@ def runstrat(): args = parse_args() - # Create a cerebro entity + # 创建 cerebro 实体 cerebro = bt.Cerebro(stdstats=False) - # Add a strategy + # 添加 strategy cerebro.addstrategy(bt.Strategy) - # Load the Data + # 加载 Data datapath = args.dataname or '../../datas/2006-day-001.txt' data = btfeeds.BacktraderCSVData( dataname=datapath) - # Handy dictionary for the argument timeframe conversion + # 用于参数 timeframe 转换的便捷字典 tframes = dict( daily=bt.TimeFrame.Days, weekly=bt.TimeFrame.Weeks, monthly=bt.TimeFrame.Months) - # Resample the data + # resample data if args.oldrs: - # Old resampler, fully deprecated + # 旧 resampler,已完全废弃 data = bt.DataResampler( dataname=data, timeframe=tframes[args.timeframe], compression=args.compression) - # Add the resample data instead of the original + # 添加 resample data,而不是原始 data cerebro.adddata(data) else: - # New resampler + # 新 resampler cerebro.resampledata( data, timeframe=tframes[args.timeframe], compression=args.compression) - # Run over everything + # 运行全部流程 cerebro.run() - # Plot the result + # 绘制结果 cerebro.plot(style='bar') diff --git a/samples/future-spot/future-spot.py b/samples/future-spot/future-spot.py index e1181c497..d86a34c09 100644 --- a/samples/future-spot/future-spot.py +++ b/samples/future-spot/future-spot.py @@ -26,7 +26,7 @@ import backtrader as bt -# The filter which changes the close price +# 修改 close price 的 filter def close_changer(data, *args, **kwargs): data.close[0] += 50.0 * random.randint(-1, 1) return False # length of stream is unchanged diff --git a/samples/gold-vs-sp500/gold-vs-sp500.py b/samples/gold-vs-sp500/gold-vs-sp500.py index 7b18675da..03bf7f306 100644 --- a/samples/gold-vs-sp500/gold-vs-sp500.py +++ b/samples/gold-vs-sp500/gold-vs-sp500.py @@ -66,7 +66,7 @@ def runstrat(args=None): # Data feed kwargs kwargs = dict() - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -105,10 +105,10 @@ def runstrat(args=None): timeframe=bt.TimeFrame.Weeks, compression=20) - # Execute + # 执行 cerebro.run(**(eval('dict(' + args.cerebro + ')'))) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**(eval('dict(' + args.plot + ')'))) @@ -130,7 +130,7 @@ def parse_args(pargs=None): parser.add_argument('--offline', required=False, action='store_true', help='Use the offline files') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='2005-01-01', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/ibtest/ibtest.py b/samples/ibtest/ibtest.py index 9951474fb..ec2f64ab8 100644 --- a/samples/ibtest/ibtest.py +++ b/samples/ibtest/ibtest.py @@ -24,7 +24,7 @@ import argparse import datetime -# The above could be sent to an independent module +# 上方内容可放到独立 module 中 import backtrader as bt from backtrader.utils import flushfile # win32 quick stdout flushing @@ -49,14 +49,14 @@ class TestStrategy(bt.Strategy): ) def __init__(self): - # To control operation entries + # 用于控制操作入口 self.orderid = list() self.order = None self.counttostop = 0 self.datastatus = 0 - # Create SMA on 2nd data + # 在第 2 个 data 上创建 SMA self.sma = bt.indicators.MovAv.SMA(self.data, period=self.p.smaperiod) print('--------------------------------------------------') @@ -211,7 +211,7 @@ def start(self): def runstrategy(): args = parse_args() - # Create a cerebro + # 创建 cerebro cerebro = bt.Cerebro() storekwargs = dict( @@ -233,7 +233,7 @@ def runstrategy(): cerebro.setbroker(broker) timeframe = bt.TimeFrame.TFrame(args.timeframe) - # Manage data1 parameters + # 管理 data1 参数 tf1 = args.timeframe1 tf1 = bt.TimeFrame.TFrame(tf1) if tf1 is not None else timeframe cp1 = args.compression1 @@ -314,7 +314,7 @@ def runstrategy(): valid = None else: valid = datetime.timedelta(seconds=args.valid) - # Add the strategy + # 添加 strategy cerebro.addstrategy(TestStrategy, smaperiod=args.smaperiod, trade=args.trade, @@ -332,7 +332,7 @@ def runstrategy(): oca=args.oca, bracket=args.bracket) - # Live data ... avoid long data accumulation by switching to "exactbars" + # live data 场景切换到 "exactbars",避免长期累积数据 cerebro.run(exactbars=args.exactbars) if args.plot and args.exactbars < 1: # plot if possible diff --git a/samples/kselrsi/ksignal.py b/samples/kselrsi/ksignal.py index 3b3124014..38df1902f 100644 --- a/samples/kselrsi/ksignal.py +++ b/samples/kselrsi/ksignal.py @@ -42,7 +42,7 @@ def notify_order(self, order): print('Close[-1]: %f - Open[0]: %f' % (d.close[-1], d.open[0])) def __init__(self): - # Original code needs artificial warmup phase - hidden sma to replic + # 原始代码需要人工 warmup 阶段;使用隐藏 sma 复现 if self.p.warmup: bt.indicators.SMA(period=self.p.warmup, plot=False) diff --git a/samples/lineplotter/lineplotter.py b/samples/lineplotter/lineplotter.py index a641824f6..5dc67e20a 100644 --- a/samples/lineplotter/lineplotter.py +++ b/samples/lineplotter/lineplotter.py @@ -48,7 +48,7 @@ def runstrat(pargs=None): cerebro = bt.Cerebro() dkwargs = dict() - # Get the dates from the args + # 从 args 获取日期 if args.fromdate is not None: fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') dkwargs['fromdate'] = fromdate @@ -62,7 +62,7 @@ def runstrat(pargs=None): cerebro.addstrategy(St, ondata=args.ondata) cerebro.run(stdstats=False) - # Plot if requested + # 按需绘图 if args.plot: pkwargs = dict(style='bar') if args.plot is not True: # evals to True but is not True diff --git a/samples/lrsi/lrsi-test.py b/samples/lrsi/lrsi-test.py index 669714312..32ba377cf 100644 --- a/samples/lrsi/lrsi-test.py +++ b/samples/lrsi/lrsi-test.py @@ -51,7 +51,7 @@ def runstrat(args=None): # Data feed kwargs kwargs = dict() - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -71,10 +71,10 @@ def runstrat(args=None): # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) - # Execute + # 执行 cerebro.run(**eval('dict(' + args.cerebro + ')')) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**eval('dict(' + args.plot + ')')) @@ -89,7 +89,7 @@ def parse_args(pargs=None): parser.add_argument('--data0', default='../../datas/2005-2006-day-001.txt', required=False, help='Data to read in') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/macd-settings/macd-settings.py b/samples/macd-settings/macd-settings.py index 799f0397a..01684e61d 100644 --- a/samples/macd-settings/macd-settings.py +++ b/samples/macd-settings/macd-settings.py @@ -32,14 +32,14 @@ class FixedPerc(bt.Sizer): - '''This sizer simply returns a fixed size for any operation + '''为每次操作返回固定现金比例对应的 size。 - Params: - - ``perc`` (default: ``0.20``) Perc of cash to allocate for operation + Args: + - ``perc`` (default: ``0.20``): 每次操作分配的现金比例。 ''' params = ( - ('perc', 0.20), # perc of cash to use for operation + ('perc', 0.20), # 每次操作使用的现金比例 ) def _getsizing(self, comminfo, cash, data, isbuy): @@ -52,27 +52,21 @@ def _getsizing(self, comminfo, cash, data, isbuy): 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: + '''该 strategy 大致参考 Van K. Tharp 的 *Trade Your Way To Financial Freedom*。 - - 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 + - 当 MACD.macd line 向上穿过 MACD.signal line,且 Simple Moving Average + 最近 x 个 periods 方向为负时进入市场。 - - If in the market: + - 在距离 close 为 x 倍 ATR 的位置设置 stop price。 - - 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 + - 持仓期间,如果当前 close 低于 stop price,则退出;否则仅当新的 stop price + 高于当前值时更新。 ''' params = ( - # Standard MACD Parameters + # 标准 MACD 参数 ('macd1', 12), ('macd2', 26), ('macdsig', 9), @@ -95,13 +89,13 @@ def __init__(self): period_me2=self.p.macd2, period_signal=self.p.macdsig) - # Cross of macd.macd and macd.signal + # macd.macd 与 macd.signal 的交叉 self.mcross = bt.indicators.CrossOver(self.macd.macd, self.macd.signal) - # To set the stop price + # 用于设置 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) @@ -126,7 +120,7 @@ def next(self): 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) @@ -156,7 +150,7 @@ def runstrat(args=None): todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') dkwargs['todate'] = todate - # if dataset is None, args.data has been given + # 如果 dataset 为 None,说明已传入 args.data dataname = DATASETS.get(args.dataset, args.data) data0 = bt.feeds.YahooFinanceCSVData(dataname=dataname, **dkwargs) cerebro.adddata(data0) @@ -171,20 +165,20 @@ def runstrat(args=None): cerebro.addsizer(FixedPerc, perc=args.cashalloc) - # Add TimeReturn Analyzers for self and the benchmark data + # 为自身和 benchmark data 添加 TimeReturn Analyzers 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 + # 为 annual returns 添加 TimeReturn Analyzers cerebro.addanalyzer(bt.analyzers.TimeReturn, timeframe=bt.TimeFrame.Years) - # Add a SharpeRatio + # 添加 SharpeRatio cerebro.addanalyzer(bt.analyzers.SharpeRatio, timeframe=bt.TimeFrame.Years, riskfreerate=args.riskfreerate) - # Add SQN to qualify the trades + # 添加 SQN 以评估 trades cerebro.addanalyzer(bt.analyzers.SQN) cerebro.addobserver(bt.observers.DrawDown) # visualize the drawdown evol @@ -270,7 +264,7 @@ def parse_args(pargs=None): 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' diff --git a/samples/multi-copy/multi-copy.py b/samples/multi-copy/multi-copy.py index a7dee85b5..c1a9b6d4a 100644 --- a/samples/multi-copy/multi-copy.py +++ b/samples/multi-copy/multi-copy.py @@ -30,15 +30,13 @@ class TheStrategy(bt.Strategy): - ''' - This strategy is capable of: + '''该 strategy 可以: - - Going Long with a Moving Average upwards CrossOver + - 在 Moving Average 向上 CrossOver 时做多。 - - Going Long again with a MACD upwards CrossOver + - 在 MACD 向上 CrossOver 时再次做多。 - - Closing the aforementioned longs with the corresponding downwards - crossovers + - 在对应的向下 crossovers 出现时关闭上述 long positions。 ''' params = ( @@ -68,10 +66,10 @@ def notify_order(self, order): print(','.join(str(x) for x in tfields)) def __init__(self): - # Choose data to buy from + # 选择用于买入的 data self.dtarget = self.getdatabyname(self.p.dtarget) - # Create indicators + # 创建 indicators sma1 = bt.ind.SMA(self.dtarget, period=self.p.sma1) sma2 = bt.ind.SMA(self.dtarget, period=self.p.sma2) self.smasig = bt.ind.CrossOver(sma1, sma2) @@ -81,7 +79,7 @@ def __init__(self): period_me2=self.p.macd2, period_signal=self.p.macdsig) - # Cross of macd.macd and macd.signal + # macd.macd 与 macd.signal 的交叉 self.macdsig = bt.ind.CrossOver(macd.macd, macd.signal) def start(self): @@ -151,7 +149,7 @@ def runstrat(args=None): todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') dkwargs['todate'] = todate - # if dataset is None, args.data has been given + # 如果 dataset 为 None,说明已传入 args.data data0 = bt.feeds.YahooFinanceCSVData(dataname=args.data0, **dkwargs) cerebro.adddata(data0, name='MyData0') @@ -230,7 +228,7 @@ def parse_args(pargs=None): 'stake=200,macd1=15,macd2=22,macdsig=7,' 'sma1=15,sma2=50')) - # Plot options + # 绘图选项 parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' diff --git a/samples/multi-example/mult-values.py b/samples/multi-example/mult-values.py index a0e82f293..37b0451f6 100644 --- a/samples/multi-example/mult-values.py +++ b/samples/multi-example/mult-values.py @@ -135,7 +135,7 @@ def runstrat(args=None): # Data feed kwargs kwargs = dict() - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -165,10 +165,10 @@ def runstrat(args=None): # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) - # Execute + # 执行 cerebro.run(**eval('dict(' + args.cerebro + ')')) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**eval('dict(' + args.plot + ')')) @@ -189,7 +189,7 @@ def parse_args(pargs=None): parser.add_argument('--data2', default='../../datas/orcl-1995-2014.txt', required=False, help='Data1 to read in') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='2001-01-01', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/multidata-strategy/multidata-strategy-unaligned.py b/samples/multidata-strategy/multidata-strategy-unaligned.py index 8097eaf9b..2ffe32d74 100644 --- a/samples/multidata-strategy/multidata-strategy-unaligned.py +++ b/samples/multidata-strategy/multidata-strategy-unaligned.py @@ -24,22 +24,19 @@ import argparse import datetime -# The above could be sent to an independent module +# 上方内容可放到独立 module 中 import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind class MultiDataStrategy(bt.Strategy): - ''' - This strategy operates on 2 datas. The expectation is that the 2 datas are - correlated and the 2nd data is used to generate signals on the 1st + '''在两个相关 datas 上运行,并使用第 2 个 data 为第 1 个 data 生成信号。 - - Buy/Sell Operationss will be executed on the 1st data - - The signals are generated using a Simple Moving Average on the 2nd data - when the close price crosses upwwards/downwards + - Buy/Sell 操作在第 1 个 data 上执行。 + - 当 close price 上下穿过第 2 个 data 的 Simple Moving Average 时生成信号。 - The strategy is a long-only strategy + 这是一个 long-only strategy。 ''' params = dict( period=15, @@ -69,21 +66,21 @@ def notify_order(self, order): self.log('%s ,' % order.Status[order.status]) pass # Simply log - # Allow new orders + # 允许新 orders self.orderid = None def __init__(self): - # To control operation entries + # 用于控制操作入口 self.orderid = None - # Create SMA on 2nd data + # 在第 2 个 data 上创建 SMA sma = btind.MovAv.SMA(self.data1, period=self.p.period) - # Create a CrossOver Signal from close an moving average + # 从 close 和 moving average 创建 CrossOver Signal self.signal = btind.CrossOver(self.data1.close, sma) def next(self): if self.orderid: - return # if an order is active, no new orders are allowed + return # 如果有 active order,则不允许新 orders if self.p.printout: print('Self len:', len(self)) @@ -115,48 +112,48 @@ def stop(self): def runstrategy(): args = parse_args() - # Create a cerebro + # 创建 cerebro cerebro = bt.Cerebro() - # Get the dates from the args + # 从 args 获取日期 fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') - # Create the 1st data + # 创建第 1 个 data data0 = btfeeds.YahooFinanceCSVData( dataname=args.data0, fromdate=fromdate, todate=todate) - # Add the 1st data to cerebro + # 添加第 1 个 data 到 cerebro cerebro.adddata(data0) - # Create the 2nd data + # 创建第 2 个 data data1 = btfeeds.YahooFinanceCSVData( dataname=args.data1, fromdate=fromdate, todate=todate) - # Add the 2nd data to cerebro + # 添加第 2 个 data 到 cerebro cerebro.adddata(data1) - # Add the strategy + # 添加 strategy cerebro.addstrategy(MultiDataStrategy, period=args.period, stake=args.stake) - # Add the commission - only stocks like a for each operation + # 添加 commission;仅针对类似 stock 的每次操作 cerebro.broker.setcash(args.cash) - # Add the commission - only stocks like a for each operation + # 添加 commission;仅针对类似 stock 的每次操作 cerebro.broker.setcommission(commission=args.commperc) - # And run it + # 然后运行 cerebro.run(runonce=not args.runnext, preload=not args.nopreload, oldsync=args.oldsync) - # Plot if requested + # 按需绘图 if args.plot: cerebro.plot(numfigs=args.numfigs, volume=False, zdown=False) diff --git a/samples/multidata-strategy/multidata-strategy.py b/samples/multidata-strategy/multidata-strategy.py index 0ccb02c3d..4fc2933ef 100644 --- a/samples/multidata-strategy/multidata-strategy.py +++ b/samples/multidata-strategy/multidata-strategy.py @@ -24,22 +24,19 @@ import argparse import datetime -# The above could be sent to an independent module +# 上方内容可放到独立 module 中 import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind class MultiDataStrategy(bt.Strategy): - ''' - This strategy operates on 2 datas. The expectation is that the 2 datas are - correlated and the 2nd data is used to generate signals on the 1st + '''在两个相关 datas 上运行,并使用第 2 个 data 为第 1 个 data 生成信号。 - - Buy/Sell Operationss will be executed on the 1st data - - The signals are generated using a Simple Moving Average on the 2nd data - when the close price crosses upwwards/downwards + - Buy/Sell 操作在第 1 个 data 上执行。 + - 当 close price 上下穿过第 2 个 data 的 Simple Moving Average 时生成信号。 - The strategy is a long-only strategy + 这是一个 long-only strategy。 ''' params = dict( period=15, @@ -69,21 +66,21 @@ def notify_order(self, order): self.log('%s ,' % order.Status[order.status]) pass # Simply log - # Allow new orders + # 允许新 orders self.orderid = None def __init__(self): - # To control operation entries + # 用于控制操作入口 self.orderid = None - # Create SMA on 2nd data + # 在第 2 个 data 上创建 SMA sma = btind.MovAv.SMA(self.data1, period=self.p.period) - # Create a CrossOver Signal from close an moving average + # 从 close 和 moving average 创建 CrossOver Signal self.signal = btind.CrossOver(self.data1.close, sma) def next(self): if self.orderid: - return # if an order is active, no new orders are allowed + return # 如果有 active order,则不允许新 orders if self.p.printout: print('Self len:', len(self)) @@ -117,48 +114,48 @@ def stop(self): def runstrategy(): args = parse_args() - # Create a cerebro + # 创建 cerebro cerebro = bt.Cerebro() - # Get the dates from the args + # 从 args 获取日期 fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') - # Create the 1st data + # 创建第 1 个 data data0 = btfeeds.YahooFinanceCSVData( dataname=args.data0, fromdate=fromdate, todate=todate) - # Add the 1st data to cerebro + # 添加第 1 个 data 到 cerebro cerebro.adddata(data0) - # Create the 2nd data + # 创建第 2 个 data data1 = btfeeds.YahooFinanceCSVData( dataname=args.data1, fromdate=fromdate, todate=todate) - # Add the 2nd data to cerebro + # 添加第 2 个 data 到 cerebro cerebro.adddata(data1) - # Add the strategy + # 添加 strategy cerebro.addstrategy(MultiDataStrategy, period=args.period, stake=args.stake) - # Add the commission - only stocks like a for each operation + # 添加 commission;仅针对类似 stock 的每次操作 cerebro.broker.setcash(args.cash) - # Add the commission - only stocks like a for each operation + # 添加 commission;仅针对类似 stock 的每次操作 cerebro.broker.setcommission(commission=args.commperc) - # And run it + # 然后运行 cerebro.run(runonce=not args.runnext, preload=not args.nopreload, oldsync=args.oldsync) - # Plot if requested + # 按需绘图 if args.plot: cerebro.plot(numfigs=args.numfigs, volume=False, zdown=False) diff --git a/samples/multitrades/multitrades.py b/samples/multitrades/multitrades.py index 29eea9599..d9a71e1af 100644 --- a/samples/multitrades/multitrades.py +++ b/samples/multitrades/multitrades.py @@ -25,7 +25,7 @@ import datetime import itertools -# The above could be sent to an independent module +# 上方内容可放到独立 module 中 import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind @@ -34,10 +34,9 @@ class MultiTradeStrategy(bt.Strategy): - '''This strategy buys/sells upong the close price crossing - upwards/downwards a Simple Moving Average. + '''根据 close price 与 Simple Moving Average 的上下交叉执行买卖。 - It can be a long-only strategy by setting the param "onlylong" to True + 将 ``onlylong`` 参数设为 ``True`` 时,可作为 long-only strategy 使用。 ''' params = dict( period=15, @@ -54,15 +53,15 @@ def log(self, txt, dt=None): print('%s, %s' % (dt.isoformat(), txt)) def __init__(self): - # To control operation entries + # 用于控制操作入口 self.order = None - # Create SMA on 2nd data + # 在第 2 个 data 上创建 SMA sma = btind.MovAv.SMA(self.data, period=self.p.period) - # Create a CrossOver Signal from close an moving average + # 从 close 和 moving average 创建 CrossOver Signal self.signal = btind.CrossOver(self.data.close, sma) - # To alternate amongst different tradeids + # 用于在不同 tradeids 之间轮换 if self.p.mtrade: self.tradeid = itertools.cycle([0, 1, 2]) else: @@ -70,7 +69,7 @@ def __init__(self): def next(self): if self.order: - return # if an order is active, no new orders are allowed + return # 如果有 active order,则不允许新 orders if self.signal > 0.0: # cross upwards if self.position: @@ -107,7 +106,7 @@ def notify_order(self, order): self.log('%s ,' % order.Status[order.status]) pass # Simply log - # Allow new orders + # 允许新 orders self.order = None def notify_trade(self, trade): @@ -122,23 +121,23 @@ def notify_trade(self, trade): def runstrategy(): args = parse_args() - # Create a cerebro + # 创建 cerebro cerebro = bt.Cerebro() - # Get the dates from the args + # 从 args 获取日期 fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') - # Create the 1st data + # 创建第 1 个 data data = btfeeds.BacktraderCSVData( dataname=args.data, fromdate=fromdate, todate=todate) - # Add the 1st data to cerebro + # 添加第 1 个 data 到 cerebro cerebro.adddata(data) - # Add the strategy + # 添加 strategy cerebro.addstrategy(MultiTradeStrategy, period=args.period, onlylong=args.onlylong, @@ -146,21 +145,21 @@ def runstrategy(): printout=args.printout, mtrade=args.mtrade) - # Add the commission - only stocks like a for each operation + # 添加 commission;仅针对类似 stock 的每次操作 cerebro.broker.setcash(args.cash) - # Add the commission - only stocks like a for each operation + # 添加 commission;仅针对类似 stock 的每次操作 cerebro.broker.setcommission(commission=args.comm, mult=args.mult, margin=args.margin) - # Add the MultiTradeObserver + # 添加 MultiTradeObserver cerebro.addobserver(mtradeobserver.MTradeObserver) - # And run it + # 然后运行 cerebro.run() - # Plot if requested + # 按需绘图 if args.plot: cerebro.plot(numfigs=args.numfigs, volume=False, zdown=False) diff --git a/samples/oandatest/oandatest.py b/samples/oandatest/oandatest.py index 4dbe5cf98..015267bbb 100644 --- a/samples/oandatest/oandatest.py +++ b/samples/oandatest/oandatest.py @@ -24,7 +24,7 @@ import argparse import datetime -# The above could be sent to an independent module +# 上方内容可放到独立 module 中 import backtrader as bt from backtrader.utils import flushfile # win32 quick stdout flushing @@ -48,14 +48,14 @@ class TestStrategy(bt.Strategy): ) def __init__(self): - # To control operation entries + # 用于控制操作入口 self.orderid = list() self.order = None self.counttostop = 0 self.datastatus = 0 - # Create SMA on 2nd data + # 在第 2 个 data 上创建 SMA self.sma = bt.indicators.MovAv.SMA(self.data, period=self.p.smaperiod) print('--------------------------------------------------') @@ -191,7 +191,7 @@ def start(self): def runstrategy(): args = parse_args() - # Create a cerebro + # 创建 cerebro cerebro = bt.Cerebro() storekwargs = dict( @@ -212,7 +212,7 @@ def runstrategy(): cerebro.setbroker(broker) timeframe = bt.TimeFrame.TFrame(args.timeframe) - # Manage data1 parameters + # 管理 data1 参数 tf1 = args.timeframe1 tf1 = bt.TimeFrame.TFrame(tf1) if tf1 is not None else timeframe cp1 = args.compression1 @@ -293,7 +293,7 @@ def runstrategy(): valid = None else: valid = datetime.timedelta(seconds=args.valid) - # Add the strategy + # 添加 strategy cerebro.addstrategy(TestStrategy, smaperiod=args.smaperiod, trade=args.trade, @@ -306,7 +306,7 @@ def runstrategy(): sell=args.sell, usebracket=args.usebracket) - # Live data ... avoid long data accumulation by switching to "exactbars" + # live data 场景切换到 "exactbars",避免长期累积数据 cerebro.run(exactbars=args.exactbars) if args.exactbars < 1: # plotting is possible if args.plot: @@ -479,7 +479,7 @@ def parse_args(pargs=None): help=('Cancel a buy order after n bars in operation,' ' to be combined with orders like Limit')) - # Plot options + # 绘图选项 parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' diff --git a/samples/observer-benchmark/observer-benchmark.py b/samples/observer-benchmark/observer-benchmark.py index 1df01e9ef..e214f5329 100644 --- a/samples/observer-benchmark/observer-benchmark.py +++ b/samples/observer-benchmark/observer-benchmark.py @@ -55,7 +55,7 @@ def start(self): def next(self): if self.p.printout: - # Print only 1st data ... is just a check that things are running + # 只打印第 1 个 data,用于确认流程正在运行 txtfields = list() txtfields.append('%04d' % len(self)) txtfields.append(self.data.datetime.datetime(0).isoformat()) @@ -187,7 +187,7 @@ def parse_args(pargs=None): default=None, choices=TIMEFRAMES.keys(), help=('TimeFrame to apply to the Observer')) - # Plot options + # 绘图选项 parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' diff --git a/samples/observers/observers-default-drawdown.py b/samples/observers/observers-default-drawdown.py index d053c7014..b65eadf34 100644 --- a/samples/observers/observers-default-drawdown.py +++ b/samples/observers/observers-default-drawdown.py @@ -44,22 +44,22 @@ def log(self, txt, dt=None): print('%s, %s' % (dt.isoformat(), txt)) def __init__(self): - # SimpleMovingAverage on main data - # Equivalent to -> sma = btind.SMA(self.data, period=self.p.smaperiod) + # 主 data 上的 SimpleMovingAverage + # 等价于 -> sma = btind.SMA(self.data, period=self.p.smaperiod) sma = btind.SMA(period=self.p.smaperiod) - # CrossOver (1: up, -1: down) close / sma + # close / sma 的 CrossOver(1: 向上,-1: 向下) self.buysell = btind.CrossOver(self.data.close, sma, plot=True) - # Sentinel to None: new ordersa allowed + # sentinel 设为 None,允许新 orders self.order = None def next(self): - # Access -1, because drawdown[0] will be calculated after "next" + # 访问 -1,因为 drawdown[0] 会在 "next" 后计算 self.log('DrawDown: %.2f' % self.stats.drawdown.drawdown[-1]) self.log('MaxDrawDown: %.2f' % self.stats.drawdown.maxdrawdown[-1]) - # Check if we are in the market + # 检查是否在市场中 if self.position: if self.buysell < 0: self.log('SELL CREATE, %.2f' % self.data.close[0]) diff --git a/samples/observers/observers-orderobserver.py b/samples/observers/observers-orderobserver.py index 5f3d2d864..cda9c0fb6 100644 --- a/samples/observers/observers-orderobserver.py +++ b/samples/observers/observers-orderobserver.py @@ -46,7 +46,7 @@ def log(self, txt, dt=None): def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: - # Buy/Sell order submitted/accepted to/by broker - Nothing to do + # Buy/Sell order 已提交/被 broker 接受,无需处理 self.log('ORDER ACCEPTED/SUBMITTED', dt=order.created.dt) self.order = order return @@ -68,26 +68,26 @@ def notify_order(self, order): order.executed.value, order.executed.comm)) - # Sentinel to None: new orders allowed + # sentinel 设为 None,允许新 orders self.order = None def __init__(self): - # SimpleMovingAverage on main data - # Equivalent to -> sma = btind.SMA(self.data, period=self.p.smaperiod) + # 主 data 上的 SimpleMovingAverage + # 等价于 -> sma = btind.SMA(self.data, period=self.p.smaperiod) sma = btind.SMA(period=self.p.smaperiod) - # CrossOver (1: up, -1: down) close / sma + # close / sma 的 CrossOver(1: 向上,-1: 向下) self.buysell = btind.CrossOver(self.data.close, sma, plot=True) - # Sentinel to None: new ordersa allowed + # sentinel 设为 None,允许新 orders self.order = None def next(self): if self.order: - # pending order ... do nothing + # 有 pending order,什么也不做 return - # Check if we are in the market + # 检查是否在市场中 if self.position: if self.buysell < 0: self.log('SELL CREATE, %.2f' % self.data.close[0]) diff --git a/samples/observers/orderobserver.py b/samples/observers/orderobserver.py index 379b25dfd..2419bc92e 100644 --- a/samples/observers/orderobserver.py +++ b/samples/observers/orderobserver.py @@ -44,9 +44,8 @@ def next(self): if not order.isbuy(): continue - # Only interested in "buy" orders, because the sell orders - # in the strategy are Market orders and will be immediately - # executed + # 只关注 "buy" orders,因为 strategy 中的 sell orders 是 Market orders, + # 会立即执行 if order.status in [bt.Order.Accepted, bt.Order.Submitted]: self.lines.created[0] = order.created.price diff --git a/samples/oco/oco.py b/samples/oco/oco.py index 3e49e9f93..b9ffcd2d9 100644 --- a/samples/oco/oco.py +++ b/samples/oco/oco.py @@ -127,7 +127,7 @@ def runstrat(args=None): # Data feed kwargs kwargs = dict() - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -147,10 +147,10 @@ def runstrat(args=None): # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) - # Execute + # 执行 cerebro.run(**eval('dict(' + args.cerebro + ')')) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**eval('dict(' + args.plot + ')')) @@ -165,7 +165,7 @@ def parse_args(pargs=None): parser.add_argument('--data0', default='../../datas/2005-2006-day-001.txt', required=False, help='Data to read in') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/optimization/optimization.py b/samples/optimization/optimization.py index 43c80458c..87344248e 100644 --- a/samples/optimization/optimization.py +++ b/samples/optimization/optimization.py @@ -40,7 +40,7 @@ class OptimizeStrategy(bt.Strategy): ) def __init__(self): - # Add indicators to add load + # 添加 indicators 以增加负载 btind.SMA(period=self.p.smaperiod) btind.MACD(period_me1=self.p.macdperiod1, @@ -51,14 +51,14 @@ def __init__(self): def runstrat(): args = parse_args() - # Create a cerebro entity + # 创建 cerebro 实体 cerebro = bt.Cerebro(maxcpus=args.maxcpus, runonce=not args.no_runonce, exactbars=args.exactbars, optdatas=not args.no_optdatas, optreturn=not args.no_optreturn) - # Add a strategy + # 添加 strategy cerebro.optstrategy( OptimizeStrategy, smaperiod=range(args.ma_low, args.ma_high), @@ -67,26 +67,26 @@ def runstrat(): macdperiod3=range(args.m3_low, args.m3_high), ) - # Get the dates from the args + # 从 args 获取日期 fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') - # Create the 1st data + # 创建第 1 个 data data = btfeeds.BacktraderCSVData( dataname=args.data, fromdate=fromdate, todate=todate) - # Add the Data Feed to Cerebro + # 添加 Data Feed 到 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('==================================================') @@ -97,7 +97,7 @@ def runstrat(): print(strat.p._getkwargs()) print('==================================================') - # print out the result + # 输出结果 print('Time used:', str(tend - tstart)) diff --git a/samples/order-close/close-daily.py b/samples/order-close/close-daily.py index 9322824bd..1b0adc803 100644 --- a/samples/order-close/close-daily.py +++ b/samples/order-close/close-daily.py @@ -62,27 +62,26 @@ def next(self): class SessionEndFiller(with_metaclass(bt.metabase.MetaParams, object)): - '''This data filter simply adds the time given in param ``endtime`` to the - current data datetime + '''为当前 data datetime 添加 ``endtime`` 指定的时间。 - It is intended for daily bars which come from sources with no time - indication and can be used to signal the bar is passed the end of the - session + 该 filter 用于来源中不包含时间信息的 daily bars,可用于标记 bar 已经过了 + session 结束时间。 - The default value for ``endtime`` is 1 second before midnight 23:59:59 + Args: + - ``endtime`` (default: ``23:59:59``): session 结束标记时间。 ''' params = (('endtime', datetime.time(23, 59, 59)),) def __call__(self, data): - ''' - Params: - - data: the data source to filter/process + '''处理一根 data bar。 + + Args: + - ``data``: 要过滤/处理的数据源。 Returns: - - False (always) because this filter does not remove bars from the - stream + bool: 始终返回 ``False``,因为该 filter 不会从 stream 中移除 bars。 ''' - # Get time of current (from data source) bar + # 获取当前 data source bar 的时间 dtime = datetime.combine(data.datetime.date(), self.p.endtime) data.datetime[0] = data.date2num(dtime) return False diff --git a/samples/order-execution/order-execution.py b/samples/order-execution/order-execution.py index c398dad64..9d6384271 100644 --- a/samples/order-execution/order-execution.py +++ b/samples/order-execution/order-execution.py @@ -51,7 +51,7 @@ def log(self, txt, dt=None): def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: - # Buy/Sell order submitted/accepted to/by broker - Nothing to do + # Buy/Sell order 已提交/被 broker 接受,无需处理 self.log('ORDER ACCEPTED/SUBMITTED', dt=order.created.dt) self.order = order return @@ -73,28 +73,28 @@ def notify_order(self, order): order.executed.value, order.executed.comm)) - # Sentinel to None: new orders allowed + # sentinel 设为 None,允许新 orders self.order = None def __init__(self): - # SimpleMovingAverage on main data - # Equivalent to -> sma = btind.SMA(self.data, period=self.p.smaperiod) + # 主 data 上的 SimpleMovingAverage + # 等价于 -> sma = btind.SMA(self.data, period=self.p.smaperiod) sma = btind.SMA(period=self.p.smaperiod) - # CrossOver (1: up, -1: down) close / sma + # close / sma 的 CrossOver(1: 向上,-1: 向下) self.buysell = btind.CrossOver(self.data.close, sma, plot=True) - # Sentinel to None: new ordersa allowed + # sentinel 设为 None,允许新 orders self.order = None def next(self): if self.order: - # An order is pending ... nothing can be done + # 有 order pending,无法继续操作 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() @@ -106,7 +106,7 @@ def next(self): 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 diff --git a/samples/order-history/order-history.py b/samples/order-history/order-history.py index e604a1430..41e2b2975 100644 --- a/samples/order-history/order-history.py +++ b/samples/order-history/order-history.py @@ -111,7 +111,7 @@ def runstrat(args=None): # Data feed kwargs kwargs = dict() - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -138,10 +138,10 @@ def runstrat(args=None): cerebro.addanalyzer(bt.analyzers.TimeReturn, timeframe=bt.TimeFrame.Years) cerebro.addanalyzer(bt.analyzers.TradeAnalyzer) - # Execute + # 执行 cerebro.run(**eval('dict(' + args.cerebro + ')')) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**eval('dict(' + args.plot + ')')) @@ -156,7 +156,7 @@ def parse_args(pargs=None): parser.add_argument('--data0', default='../../datas/2005-2006-day-001.txt', required=False, help='Data to read in') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/order_target/order_target.py b/samples/order_target/order_target.py index 3666390a0..e8e38034f 100644 --- a/samples/order_target/order_target.py +++ b/samples/order_target/order_target.py @@ -28,23 +28,17 @@ 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: + '''该 strategy 大致参考 Van K. Tharp 的 *Trade Your Way To Financial Freedom*。 - - 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 + - 当 MACD.macd line 向上穿过 MACD.signal line,且 Simple Moving Average + 最近 x 个 periods 方向为负时进入市场。 - - If in the market: + - 在距离 close 为 x 倍 ATR 的位置设置 stop price。 - - 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 + - 持仓期间,如果当前 close 低于 stop price,则退出;否则仅当新的 stop price + 高于当前值时更新。 ''' params = ( @@ -179,7 +173,7 @@ def parse_args(pargs=None): action='store_true', help=('Use order_target_percent')) - # Plot options + # 绘图选项 parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' diff --git a/samples/partial-plot/partial-plot.py b/samples/partial-plot/partial-plot.py index 0c67f1097..f49e76451 100644 --- a/samples/partial-plot/partial-plot.py +++ b/samples/partial-plot/partial-plot.py @@ -34,7 +34,7 @@ class St(bt.Strategy): def __init__(self): # self.schedule_once(self.pepe, when=datetime.datetime()) - # This one won't have the expected fidelity in backtesting + # 这一项在 backtesting 中不会达到预期精度 # self.schedule_once(self.pepe, when=datetime.timedelta()) # self.schedule_reps(self.pepe, when=datetime.time(), days=bt.sched.) @@ -54,7 +54,7 @@ def runstrat(args=None): # Data feed kwargs kwargs = dict() - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -76,10 +76,10 @@ def runstrat(args=None): # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) - # Execute + # 执行 cerebro.run(**eval('dict(' + args.cerebro + ')')) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**eval('dict(' + args.plot + ')')) @@ -94,7 +94,7 @@ def parse_args(pargs=None): parser.add_argument('--data0', default='../../datas/2005-2006-day-001.txt', required=False, help='Data to read in') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/pinkfish-challenge/pinkfish-challenge.py b/samples/pinkfish-challenge/pinkfish-challenge.py index bf43aee8c..fc0e78af2 100644 --- a/samples/pinkfish-challenge/pinkfish-challenge.py +++ b/samples/pinkfish-challenge/pinkfish-challenge.py @@ -53,13 +53,13 @@ def __init__(self, data): self.pendingbar = None def __call__(self, data): - # Make a copy of the new bar and remove it from stream + # 复制新 bar,并从 stream 中移除 closebar = [data.lines[i][0] for i in range(data.size())] datadt = data.datetime.date() # keep the date ohlbar = closebar[:] # Make an open-high-low bar - # Adjust volume + # 调整 volume ohlbar[data.Volume] = int(closebar[data.Volume] * (1.0 - self.p.cvol)) dt = datetime.datetime.combine(datadt, data.p.sessionstart) @@ -68,28 +68,29 @@ def __call__(self, data): dt = datetime.datetime.combine(datadt, data.p.sessionend) closebar[data.DateTime] = data.date2num(dt) - # Update stream + # 更新 stream data.backwards() # remove the copied bar from stream - # Overwrite the new data bar with our pending data - except start point + # 用 pending data 覆盖新 data bar,但起点除外 if self.pendingbar is not None: data._updatebar(self.pendingbar) self.pendingbar = closebar # update the pending bar to the new bar - data._add2stack(ohlbar) # Add the openbar to the stack for processing + data._add2stack(ohlbar) # 将 openbar 加入 stack 等待处理 return False # the length of the stream was not changed def last(self, data): - '''Called when the data is no longer producing bars - Can be called multiple times. It has the chance to (for example) - produce extra bars''' + '''在 data 不再产生 bars 时调用。 + + 该方法可能被多次调用,并有机会产生额外 bars。 + ''' if self.pendingbar is not None: - data.backwards() # remove delivered open bar - data._add2stack(self.pendingbar) # add remaining - self.pendingbar = None # No further action - return True # something delivered + data.backwards() # 移除已交付的 open bar + data._add2stack(self.pendingbar) # 加入剩余部分 + self.pendingbar = None # 无需后续操作 + return True # 已交付内容 - return False # nothing delivered here + return False # 此处没有交付内容 class DayStepsReplayFilter(bt.with_metaclass(bt.MetaParams, object)): @@ -120,7 +121,7 @@ def __init__(self, data): pass def __call__(self, data): - # Make a copy of the new bar and remove it from stream + # 复制新 bar,并从 stream 中移除 datadt = data.datetime.date() # keep the date if self.lastdt == datadt: @@ -128,11 +129,11 @@ def __call__(self, data): self.lastdt = datadt # keep ref to last seen bar - # Make a copy of current data for ohlbar + # 为 ohlbar 复制当前 data 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 + # 用 o-h-l 平均值替换 close price ohlprice = ohlbar[data.Open] + ohlbar[data.High] + ohlbar[data.Low] ohlbar[data.Close] = ohlprice / 3.0 @@ -142,25 +143,25 @@ def __call__(self, data): 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) - # Ajust closebar to generate a single tick -> close price + # 调整 closebar,使其生成单个 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 + # 更新 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 + # 将第 2 部分加入 stash,延迟到下一轮处理 data._add2stack(closebar, stash=True) return False # the length of the stream was not changed @@ -194,7 +195,7 @@ def start(self): self.lcontrol = 0 # control if 1st or 2nd call self.inmarket = 0 - # Get the highest but delayed 1 ... to avoid "today" + # 获取 highest 并延迟 1,以避开“今天” self.highest = btind.Highest(self.data.high, period=self.p.highperiod, subplot=False) @@ -326,7 +327,7 @@ def parse_args(pargs=None): parser.add_argument('--oldbuysell', required=False, action='store_true', help=('Old buysell plot behavior - ON THE PRICE')) - # Plot options + # 绘图选项 parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' diff --git a/samples/plot-same-axis/plot-same-axis.py b/samples/plot-same-axis/plot-same-axis.py index c67c88963..9be36a1c5 100644 --- a/samples/plot-same-axis/plot-same-axis.py +++ b/samples/plot-same-axis/plot-same-axis.py @@ -24,16 +24,14 @@ import argparse import datetime -# The above could be sent to an independent module +# 上方内容可放到独立 module 中 import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind class PlotStrategy(bt.Strategy): - ''' - The strategy does nothing but create indicators for plotting purposes - ''' + '''仅创建用于绘图的 indicators,不执行交易。''' params = dict( smasubplot=False, # default for Moving averages nomacdplot=False, @@ -66,23 +64,23 @@ def __init__(self): def runstrategy(): args = parse_args() - # Create a cerebro + # 创建 cerebro cerebro = bt.Cerebro() - # Get the dates from the args + # 从 args 获取日期 fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') - # Create the 1st data + # 创建第 1 个 data data = btfeeds.BacktraderCSVData( dataname=args.data, fromdate=fromdate, todate=todate) - # Add the 1st data to cerebro + # 添加第 1 个 data 到 cerebro cerebro.adddata(data) - # Add the strategy + # 添加 strategy cerebro.addstrategy(PlotStrategy, smasubplot=args.smasubplot, nomacdplot=args.nomacdplot, @@ -91,7 +89,7 @@ def runstrategy(): stocrsi=args.stocrsi, stocrsilabels=args.stocrsilabels) - # And run it + # 然后运行 cerebro.run(stdstats=args.stdstats) # Plot diff --git a/samples/psar/psar-intraday.py b/samples/psar/psar-intraday.py index f16af16b0..73e5a85e6 100644 --- a/samples/psar/psar-intraday.py +++ b/samples/psar/psar-intraday.py @@ -66,7 +66,7 @@ def runstrat(args=None): compression=5, ) - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -88,10 +88,10 @@ def runstrat(args=None): # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) - # Execute + # 执行 cerebro.run(**eval('dict(' + args.cerebro + ')')) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**eval('dict(' + args.plot + ')')) @@ -106,7 +106,7 @@ def parse_args(pargs=None): parser.add_argument('--data0', default='../../datas//2006-min-005.txt', required=False, help='Data to read in') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/psar/psar.py b/samples/psar/psar.py index 7140d7e9f..4886cd740 100644 --- a/samples/psar/psar.py +++ b/samples/psar/psar.py @@ -51,7 +51,7 @@ def runstrat(args=None): # Data feed kwargs kwargs = dict() - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -71,10 +71,10 @@ def runstrat(args=None): # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) - # Execute + # 执行 cerebro.run(**eval('dict(' + args.cerebro + ')')) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**eval('dict(' + args.plot + ')')) @@ -89,7 +89,7 @@ def parse_args(pargs=None): parser.add_argument('--data0', default='../../datas/2005-2006-day-001.txt', required=False, help='Data to read in') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/pyfolio2/pyfoliotest.py b/samples/pyfolio2/pyfoliotest.py index 681f94602..474497d39 100644 --- a/samples/pyfolio2/pyfoliotest.py +++ b/samples/pyfolio2/pyfoliotest.py @@ -65,7 +65,7 @@ def start(self): def next(self): super(self.__class__, self).next() if self.p.printdata: - # Print only 1st data ... is just a check that things are running + # 只打印第 1 个 data,用于确认流程正在运行 txtfields = list() txtfields.append('%04d' % len(self)) txtfields.append(self.data.datetime.datetime(0).isoformat()) @@ -119,7 +119,7 @@ def runstrat(args=None): cerebro.addstrategy(St, short=args.short, printdata=args.printdata) cerebro.addsizer(bt.sizers.FixedSize, stake=args.stake) - # Own analyzerset + # 自有 analyzerset cerebro.addanalyzer(bt.analyzers.TimeReturn, timeframe=bt.TimeFrame.Years) cerebro.addanalyzer(bt.analyzers.SharpeRatio, timeframe=bt.TimeFrame.Years) cerebro.addanalyzer(bt.analyzers.SQN,) @@ -135,7 +135,7 @@ def runstrat(args=None): print('End Run') strat = results[0] - # Results of own analyzers + # 自有 analyzers 的结果 al = strat.analyzers.timereturn print('-- Time Return:') for k, v in al.get_analysis().items(): @@ -231,7 +231,7 @@ def parse_args(pargs=None): parser.add_argument('--printdata', required=False, action='store_true', help=('Print data lines')) - # Plot options + # 绘图选项 parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' diff --git a/samples/pyfoliotest/pyfoliotest.py b/samples/pyfoliotest/pyfoliotest.py index 2ee6701ff..99979799c 100644 --- a/samples/pyfoliotest/pyfoliotest.py +++ b/samples/pyfoliotest/pyfoliotest.py @@ -53,7 +53,7 @@ def start(self): def next(self): if self.p.printout: - # Print only 1st data ... is just a check that things are running + # 只打印第 1 个 data,用于确认流程正在运行 txtfields = list() txtfields.append('%04d' % len(self)) txtfields.append(self.data.datetime.datetime(0).isoformat()) diff --git a/samples/relative-volume/relative-volume.py b/samples/relative-volume/relative-volume.py index b285735d5..e3f261182 100644 --- a/samples/relative-volume/relative-volume.py +++ b/samples/relative-volume/relative-volume.py @@ -24,7 +24,7 @@ import argparse import datetime -# The above could be sent to an independent module +# 上方内容可放到独立 module 中 import backtrader as bt import backtrader.feeds as btfeeds @@ -34,43 +34,43 @@ def runstrategy(): args = parse_args() - # Create a cerebro + # 创建 cerebro cerebro = bt.Cerebro() - # Get the dates from the args + # 从 args 获取日期 fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') - # Create the 1st data + # 创建第 1 个 data data = btfeeds.BacktraderCSVData( dataname=args.data, fromdate=fromdate, todate=todate, ) - # Add the 1st data to cerebro + # 添加第 1 个 data 到 cerebro cerebro.adddata(data) - # Add an empty strategy + # 添加空 strategy cerebro.addstrategy(bt.Strategy) - # Get the session times to pass them to the indicator + # 获取 session 时间并传给 indicator prestart = datetime.datetime.strptime(args.prestart, '%H:%M').time() start = datetime.datetime.strptime(args.start, '%H:%M').time() end = datetime.datetime.strptime(args.end, '%H:%M').time() - # Add the Relative volume indicator + # 添加 Relative volume indicator cerebro.addindicator(RelativeVolumeByBar, prestart=prestart, start=start, end=end) - # Add a writer with CSV + # 添加带 CSV 的 writer if args.writer: cerebro.addwriter(bt.WriterFile, csv=args.wrcsv) - # And run it + # 然后运行 cerebro.run(stdstats=False) - # Plot if requested + # 按需绘图 if args.plot: cerebro.plot(numfigs=args.numfigs, volume=True) diff --git a/samples/relative-volume/relvolbybar.py b/samples/relative-volume/relvolbybar.py index 0ab5c91ca..2fd8e4ce4 100644 --- a/samples/relative-volume/relvolbybar.py +++ b/samples/relative-volume/relvolbybar.py @@ -47,18 +47,18 @@ def _plotlabel(self): return plabels def __init__(self): - # Inform the platform about the minimum period needs + # 通知平台所需的 minimum period minbuffer = self._calcbuffer() self.addminperiod(minbuffer) - # Structures/variable to keep synchronization + # 用于保持同步的结构/变量 self.pvol = dict() self.vcount = collections.defaultdict(int) self.days = 0 self.dtlast = datetime.date.min - # Done after calc to ensure coop inheritance and composition work + # 在计算后完成,以确保协作继承和组合正常工作 super(RelativeVolumeByBar, self).__init__() def _barisvalid(self, tm): @@ -85,27 +85,27 @@ def next(self): if not self._barisvalid(tm): return - # Record the "minute/second" of this day has been seen + # 记录当天已出现的 "minute/second" self.vcount[tm] += 1 - # Get the bar's volume + # 获取 bar 的 volume vol = self.data.volume[0] - # If number of days is right, we saw the same "minute/second" last day + # 如果天数正确,说明上一天见过相同 "minute/second" if self.vcount[tm] == self.days: self.lines.rvbb[0] = vol / self.pvol[tm] - # Synchronize the days and volume count for next cycle + # 同步 days 和 volume count,供下一轮使用 self.vcount[tm] = self.days - # Record the volume for this bar for next cycle + # 记录当前 bar 的 volume,供下一轮使用 self.pvol[tm] = vol def _calcbuffer(self): - # Period calculation + # period 计算 minend = self.p.end.hour * 60 + self.p.end.minute # minstart = session_start.hour * 60 + session_start.minute - # use prestart to account for market_data + # 使用 prestart 以考虑 market_data minstart = self.p.prestart.hour * 60 + self.p.prestart.minute minbuffer = minend - minstart diff --git a/samples/renko/renko.py b/samples/renko/renko.py index 6a662fffe..f9aa02d10 100644 --- a/samples/renko/renko.py +++ b/samples/renko/renko.py @@ -47,7 +47,7 @@ def runstrat(args=None): # Data feed kwargs kwargs = dict() - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -77,12 +77,12 @@ def runstrat(args=None): # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) - # Execute + # 执行 kwargs = dict(stdstats=False) kwargs.update(**eval('dict(' + args.cerebro + ')')) cerebro.run(**kwargs) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to kwargs = dict(style='candle') kwargs.update(**eval('dict(' + args.plot + ')')) cerebro.plot(**kwargs) @@ -99,7 +99,7 @@ def parse_args(pargs=None): parser.add_argument('--data0', default='../../datas/2005-2006-day-001.txt', required=False, help='Data to read in') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/resample-tickdata/resample-tickdata.py b/samples/resample-tickdata/resample-tickdata.py index 39f4d2dae..9f8ff968a 100644 --- a/samples/resample-tickdata/resample-tickdata.py +++ b/samples/resample-tickdata/resample-tickdata.py @@ -30,13 +30,13 @@ def runstrat(): args = parse_args() - # Create a cerebro entity + # 创建 cerebro 实体 cerebro = bt.Cerebro(stdstats=False) - # Add a strategy + # 添加 strategy cerebro.addstrategy(bt.Strategy) - # Load the Data + # 加载 Data datapath = args.dataname or '../../datas/ticksample.csv' data = btfeeds.GenericCSVData( @@ -45,7 +45,7 @@ def runstrat(): timeframe=bt.TimeFrame.Ticks, ) - # Handy dictionary for the argument timeframe conversion + # 用于参数 timeframe 转换的便捷字典 tframes = dict( ticks=bt.TimeFrame.Ticks, microseconds=bt.TimeFrame.MicroSeconds, @@ -55,7 +55,7 @@ def runstrat(): weekly=bt.TimeFrame.Weeks, monthly=bt.TimeFrame.Months) - # Resample the data + # resample data cerebro.resampledata( data, timeframe=tframes[args.timeframe], @@ -65,13 +65,13 @@ def runstrat(): rightedge=args.rightedge) if args.writer: - # add a writer + # 添加 writer cerebro.addwriter(bt.WriterFile, csv=args.wrcsv) - # Run over everything + # 运行全部流程 cerebro.run() - # Plot the result + # 绘制结果 cerebro.plot(style='bar') diff --git a/samples/rollover/rollover.py b/samples/rollover/rollover.py index d287d5fd3..1299eefbe 100644 --- a/samples/rollover/rollover.py +++ b/samples/rollover/rollover.py @@ -40,7 +40,7 @@ def next(self): txt = list() txt.append('%04d' % len(self.data0)) txt.append('{}'.format(self.data0._dataname)) - # Internal knowledge ... current expiration in use is in _d + # 内部约定:当前使用的 expiration 保存在 _d txt.append('{}'.format(self.data0._d._dataname)) txt.append('{}'.format(self.data.datetime.date())) txt.append('{}'.format(self.data.datetime.date().strftime('%a'))) @@ -54,7 +54,7 @@ def next(self): def checkdate(dt, d): - # Check if the date is in the week where the 3rd friday of Mar/Jun/Sep/Dec + # 检查日期是否位于 3/6/9/12 月第 3 个周五所在周 # EuroStoxx50 expiry codes: MY # M -> H, M, U, Z (Mar, Jun, Sep, Dec) @@ -74,14 +74,14 @@ def checkdate(dt, d): exp_day = 21 - (calendar.weekday(Y, M, 1) + 2) % 7 exp_dt = datetime.datetime(Y, M, exp_day) - # Get the year, week numbers + # 获取年份和周数 exp_year, exp_week, _ = exp_dt.isocalendar() dt_year, dt_week, _ = dt.isocalendar() # print('dt {} vs {} exp_dt'.format(dt, exp_dt)) # print('dt_week {} vs {} exp_week'.format(dt_week, exp_week)) - # can switch if in same week + # 如果处于同一周则可以切换 return (dt_year, dt_week) == (exp_year, exp_week) @@ -144,7 +144,7 @@ def parse_args(pargs=None): action='store_true', help='Change when a given condition is met') - # Plot options + # 绘图选项 parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' diff --git a/samples/sharpe-timereturn/sharpe-timereturn.py b/samples/sharpe-timereturn/sharpe-timereturn.py index 0922c2302..959f23ba1 100644 --- a/samples/sharpe-timereturn/sharpe-timereturn.py +++ b/samples/sharpe-timereturn/sharpe-timereturn.py @@ -30,25 +30,25 @@ def runstrat(pargs=None): args = parse_args(pargs) - # Create a cerebro + # 创建 cerebro cerebro = bt.Cerebro() if args.cash is not None: cerebro.broker.set_cash(args.cash) - # Get the dates from the args + # 从 args 获取日期 fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') - # Create the 1st data + # 创建第 1 个 data data = bt.feeds.BacktraderCSVData( dataname=args.data, fromdate=fromdate, todate=todate) - cerebro.adddata(data) # Add the data to cerebro + cerebro.adddata(data) # 添加 data 到 cerebro - # Add the strategy + # 添加 strategy cerebro.addstrategy(bt.strategies.SMA_CrossOver) tframes = dict( @@ -57,7 +57,7 @@ def runstrat(pargs=None): months=bt.TimeFrame.Months, years=bt.TimeFrame.Years) - # Add the Analyzers + # 添加 Analyzers cerebro.addanalyzer(bt.analyzers.TimeReturn, timeframe=tframes[args.tframe]) @@ -81,12 +81,12 @@ def runstrat(pargs=None): timeframe=tframes[args.tframe], **shkwargs) - # Add a writer to get output + # 添加 writer 以获取输出 cerebro.addwriter(bt.WriterFile, csv=args.writercsv, rounding=4) - cerebro.run() # And run it + cerebro.run() # 然后运行 - # Plot if requested + # 按需绘图 if args.plot: pkwargs = dict(style='bar') if args.plot is not True: # evals to True but is not True @@ -144,7 +144,7 @@ def parse_args(pargs=None): help=('Upgrade returns to target timeframe rather than' 'downgrading the riskfreerate')) - # Plot options + # 绘图选项 parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' diff --git a/samples/signals-strategy/signals-strategy.py b/samples/signals-strategy/signals-strategy.py index a556f3e51..535674f73 100644 --- a/samples/signals-strategy/signals-strategy.py +++ b/samples/signals-strategy/signals-strategy.py @@ -73,7 +73,7 @@ def runstrat(args=None): todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') dkwargs['todate'] = todate - # if dataset is None, args.data has been given + # 如果 dataset 为 None,说明已传入 args.data data = bt.feeds.BacktraderCSVData(dataname=args.data, **dkwargs) cerebro.adddata(data) @@ -132,7 +132,7 @@ def parse_args(pargs=None): default=None, choices=EXITSIGNALS, help=('Signal type to use for the exit signal')) - # Plot options + # 绘图选项 parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' diff --git a/samples/sizertest/sizertest.py b/samples/sizertest/sizertest.py index 17ab3610b..dbfbe7e56 100644 --- a/samples/sizertest/sizertest.py +++ b/samples/sizertest/sizertest.py @@ -50,7 +50,7 @@ def _getsizing(self, comminfo, cash, data, isbuy): if isbuy: return self.p.stake - # Sell situation + # sell 情境 position = self.broker.getposition(data) if not position.size: return 0 # do not sell if nothing is open @@ -134,7 +134,7 @@ def parse_args(pargs=None): type=int, default=15, help=('Period for the Simple Moving Average')) - # Plot options + # 绘图选项 parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' diff --git a/samples/slippage/slippage.py b/samples/slippage/slippage.py index 2c9c88a38..7718c5ccd 100644 --- a/samples/slippage/slippage.py +++ b/samples/slippage/slippage.py @@ -67,7 +67,7 @@ def runstrat(args=None): todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') dkwargs['todate'] = todate - # if dataset is None, args.data has been given + # 如果 dataset 为 None,说明已传入 args.data data = bt.feeds.BacktraderCSVData(dataname=args.data, **dkwargs) cerebro.adddata(data) @@ -150,7 +150,7 @@ def parse_args(pargs=None): parser.add_argument('--slip_open', required=False, action='store_true', help=('Slip even if match price is next open')) - # Plot options + # 绘图选项 parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' diff --git a/samples/sratio/sratio.py b/samples/sratio/sratio.py index 70d09da19..d9cc92d18 100644 --- a/samples/sratio/sratio.py +++ b/samples/sratio/sratio.py @@ -36,7 +36,7 @@ def run(pargs=None): print('returns is:', returns, ' - retfree is:', retfree) - # Directly from backtrader + # 直接来自 backtrader retfree = itertools.repeat(retfree) ret_free = map(operator.sub, returns, retfree) # excess returns ret_free_avg = average(list(ret_free)) # mean of the excess returns diff --git a/samples/stop-trading/stop-loss-approaches.py b/samples/stop-trading/stop-loss-approaches.py index 48d6cd512..9b7fa6439 100644 --- a/samples/stop-trading/stop-loss-approaches.py +++ b/samples/stop-trading/stop-loss-approaches.py @@ -34,10 +34,10 @@ class BaseStrategy(bt.Strategy): ) def __init__(self): - # omitting a data implies self.datas[0] (aka self.data and self.data0) + # 省略 data 表示使用 self.datas[0](即 self.data 和 self.data0) fast_ma = bt.ind.EMA(period=self.p.fast_ma) slow_ma = bt.ind.EMA(period=self.p.slow_ma) - # our entry point + # 入场点 self.crossup = bt.ind.CrossUp(fast_ma, slow_ma) @@ -55,7 +55,7 @@ def notify_order(self, order): print('SELL@price: {:.2f}'.format(order.executed.price)) return - # We have entered the market + # 已进入市场 print('BUY @price: {:.2f}'.format(order.executed.price)) if not self.p.trail: @@ -66,7 +66,7 @@ def notify_order(self, order): def next(self): if not self.position and self.crossup > 0: - # not in the market and signal triggered + # 不在市场中且信号已触发 self.buy() @@ -88,12 +88,12 @@ def notify_order(self, order): print('SELL@price: {:.2f}'.format(order.executed.price)) return - # We have entered the market + # 已进入市场 print('BUY @price: {:.2f}'.format(order.executed.price)) def next(self): if not self.position and self.crossup > 0: - # not in the market and signal triggered + # 不在市场中且信号已触发 self.buy() if not self.p.trail: @@ -126,7 +126,7 @@ def notify_order(self, order): print('SELL@price: {:.2f}'.format(order.executed.price)) return - # We have entered the market + # 已进入市场 print('BUY @price: {:.2f}'.format(order.executed.price)) def next(self): @@ -134,17 +134,17 @@ def next(self): if self.buy_order: # something was pending self.cancel(self.buy_order) - # not in the market and signal triggered + # 不在市场中且信号已触发 if not self.p.buy_limit: self.buy_order = self.buy(transmit=False) else: price = self.data.close[0] * (1.0 - self.p.buy_limit) - # transmit = False ... await child order before transmission + # transmit = False:等待子 order 后再提交 self.buy_order = self.buy(price=price, exectype=bt.Order.Limit, transmit=False) - # Setting parent=buy_order ... sends both together + # 设置 parent=buy_order 会将两者一起发送 if not self.p.trail: stop_price = self.data.close[0] * (1.0 - self.p.stop_loss) self.sell(exectype=bt.Order.Stop, price=stop_price, @@ -170,7 +170,7 @@ def runstrat(args=None): # Data feed kwargs kwargs = dict() - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -190,10 +190,10 @@ def runstrat(args=None): StClass = APPROACHES[args.approach] cerebro.addstrategy(StClass, **eval('dict(' + args.strat + ')')) - # Execute + # 执行 cerebro.run(**eval('dict(' + args.cerebro + ')')) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**eval('dict(' + args.plot + ')')) @@ -208,11 +208,11 @@ def parse_args(pargs=None): parser.add_argument('--data0', default='../../datas/2005-2006-day-001.txt', required=False, help='Data to read in') - # Strategy to choose + # 要选择的 strategy parser.add_argument('approach', choices=APPROACHES.keys(), help='Stop approach to use') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/stoptrail/trail.py b/samples/stoptrail/trail.py index e4e7f3fe5..24d268e2b 100644 --- a/samples/stoptrail/trail.py +++ b/samples/stoptrail/trail.py @@ -94,7 +94,7 @@ def runstrat(args=None): # Data feed kwargs kwargs = dict() - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -114,10 +114,10 @@ def runstrat(args=None): # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) - # Execute + # 执行 cerebro.run(**eval('dict(' + args.cerebro + ')')) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**eval('dict(' + args.plot + ')')) @@ -132,7 +132,7 @@ def parse_args(pargs=None): parser.add_argument('--data0', default='../../datas/2005-2006-day-001.txt', required=False, help='Data to read in') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/talib/tablibsartest.py b/samples/talib/tablibsartest.py index 701f76ac4..3bbc4755b 100644 --- a/samples/talib/tablibsartest.py +++ b/samples/talib/tablibsartest.py @@ -83,7 +83,7 @@ def parse_args(pargs=None): help=('Use next (step by step) ' 'instead of once (batch)')) - # Plot options + # 绘图选项 parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' diff --git a/samples/talib/talibtest.py b/samples/talib/talibtest.py index 1387fdb29..b325e641f 100644 --- a/samples/talib/talibtest.py +++ b/samples/talib/talibtest.py @@ -173,7 +173,7 @@ def parse_args(pargs=None): help=('Use next (step by step) ' 'instead of once (batch)')) - # Plot options + # 绘图选项 parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' diff --git a/samples/timers/scheduled-min.py b/samples/timers/scheduled-min.py index 80d2e6633..18f284021 100644 --- a/samples/timers/scheduled-min.py +++ b/samples/timers/scheduled-min.py @@ -108,7 +108,7 @@ def runstrat(args=None): sessionend=datetime.time(17, 30), ) - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -128,10 +128,10 @@ def runstrat(args=None): # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) - # Execute + # 执行 cerebro.run(**eval('dict(' + args.cerebro + ')')) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**eval('dict(' + args.plot + ')')) @@ -146,7 +146,7 @@ def parse_args(pargs=None): parser.add_argument('--data0', default='../../datas/2006-min-005.txt', required=False, help='Data to read in') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/timers/scheduled.py b/samples/timers/scheduled.py index 4030701eb..3122d532c 100644 --- a/samples/timers/scheduled.py +++ b/samples/timers/scheduled.py @@ -96,7 +96,7 @@ def runstrat(args=None): sessionend=datetime.time(17, 30), ) - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -116,10 +116,10 @@ def runstrat(args=None): # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) - # Execute + # 执行 cerebro.run(**eval('dict(' + args.cerebro + ')')) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**eval('dict(' + args.plot + ')')) @@ -134,7 +134,7 @@ def parse_args(pargs=None): parser.add_argument('--data0', default='../../datas/2005-2006-day-001.txt', required=False, help='Data to read in') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/tradingcalendar/tcal-intra.py b/samples/tradingcalendar/tcal-intra.py index 83c35f5f1..e360f7b25 100644 --- a/samples/tradingcalendar/tcal-intra.py +++ b/samples/tradingcalendar/tcal-intra.py @@ -87,7 +87,7 @@ def runstrat(args=None): tz = 'US/Eastern' kwargs = dict(tzinput=tzinput, tz=tz) - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -117,10 +117,10 @@ def runstrat(args=None): # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) - # Execute + # 执行 cerebro.run(**eval('dict(' + args.cerebro + ')')) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**eval('dict(' + args.plot + ')')) @@ -135,7 +135,7 @@ def parse_args(pargs=None): parser.add_argument('--data0', default='yhoo-2016-11.csv', required=False, help='Data to read in') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='2016-01-01', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/tradingcalendar/tcal.py b/samples/tradingcalendar/tcal.py index 832b4b499..1b9c8be3b 100644 --- a/samples/tradingcalendar/tcal.py +++ b/samples/tradingcalendar/tcal.py @@ -82,7 +82,7 @@ def runstrat(args=None): # Data feed kwargs kwargs = dict() - # Parse from/to-date + # 解析 from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: @@ -116,10 +116,10 @@ def runstrat(args=None): # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) - # Execute + # 执行 cerebro.run(**eval('dict(' + args.cerebro + ')')) - if args.plot: # Plot if requested to + if args.plot: # 按需绘图 to cerebro.plot(**eval('dict(' + args.plot + ')')) @@ -137,7 +137,7 @@ def parse_args(pargs=None): parser.add_argument('--offline', required=False, action='store_true', help='Read from disk with same name as ticker') - # Defaults for dates + # 日期默认值 parser.add_argument('--fromdate', required=False, default='2016-01-01', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') diff --git a/samples/vctest/vctest.py b/samples/vctest/vctest.py index 66adf4b26..7b6fb8cd2 100644 --- a/samples/vctest/vctest.py +++ b/samples/vctest/vctest.py @@ -24,7 +24,7 @@ import argparse import datetime -# The above could be sent to an independent module +# 上方内容可放到独立 module 中 import backtrader as bt from backtrader.utils import flushfile # win32 quick stdout flushing from backtrader.utils.py3 import string_types @@ -45,14 +45,14 @@ class TestStrategy(bt.Strategy): ) def __init__(self): - # To control operation entries + # 用于控制操作入口 self.orderid = list() self.order = None self.counttostop = 0 self.datastatus = 0 - # Create SMA on 2nd data + # 在第 2 个 data 上创建 SMA self.sma = bt.indicators.MovAv.SMA(self.data, period=self.p.smaperiod) print('--------------------------------------------------') @@ -155,7 +155,7 @@ def start(self): def runstrategy(): args = parse_args() - # Create a cerebro + # 创建 cerebro cerebro = bt.Cerebro() storekwargs = dict() @@ -245,7 +245,7 @@ def runstrategy(): else: valid = datetime.timedelta(seconds=args.valid) - # Add the strategy + # 添加 strategy cerebro.addstrategy(TestStrategy, smaperiod=args.smaperiod, trade=args.trade, @@ -258,7 +258,7 @@ def runstrategy(): price=args.price, pstoplimit=args.pstoplimit) - # Live data ... avoid long data accumulation by switching to "exactbars" + # live data 场景切换到 "exactbars",避免长期累积数据 cerebro.run(exactbars=args.exactbars) if args.plot and args.exactbars < 1: # plot if possible diff --git a/samples/volumefilling/volumefilling.py b/samples/volumefilling/volumefilling.py index 04395ab0e..684384527 100644 --- a/samples/volumefilling/volumefilling.py +++ b/samples/volumefilling/volumefilling.py @@ -77,7 +77,7 @@ def next(self): txtfields.append('%.2f' % self.data0.openinterest[0]) print(','.join(txtfields)) - # Single order + # 单个 order if self.doop == 0: if not self.position.size: stakevol = (self.data0.volume[0] * self.p.stakeperc) // 100 diff --git a/samples/vwr/vwr.py b/samples/vwr/vwr.py index 0ac9965b8..d853f91ed 100644 --- a/samples/vwr/vwr.py +++ b/samples/vwr/vwr.py @@ -36,14 +36,14 @@ def runstrat(pargs=None): args = parse_args(pargs) - # Create a cerebro + # 创建 cerebro cerebro = bt.Cerebro() if args.cash is not None: cerebro.broker.set_cash(args.cash) dkwargs = dict() - # Get the dates from the args + # 从 args 获取日期 if args.fromdate is not None: fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') dkwargs['fromdate'] = fromdate @@ -51,11 +51,11 @@ def runstrat(pargs=None): todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') dkwargs['todate'] = todate - # Create the 1st data + # 创建第 1 个 data data = bt.feeds.BacktraderCSVData(dataname=args.data, **dkwargs) - cerebro.adddata(data) # Add the data to cerebro + cerebro.adddata(data) # 添加 data 到 cerebro - cerebro.addstrategy(bt.strategies.SMA_CrossOver) # Add the strategy + cerebro.addstrategy(bt.strategies.SMA_CrossOver) # 添加 strategy lrkwargs = dict() if args.tframe is not None: @@ -88,12 +88,12 @@ def runstrat(pargs=None): cerebro.addanalyzer(bt.analyzers.TimeReturn, timeframe=bt.TimeFrame.Years) - # Add a writer to get output + # 添加 writer 以获取输出 cerebro.addwriter(bt.WriterFile, csv=args.writercsv, rounding=4) - cerebro.run() # And run it + cerebro.run() # 然后运行 - # Plot if requested + # 按需绘图 if args.plot: pkwargs = dict(style='bar') if args.plot is not True: # evals to True but is not True @@ -145,7 +145,7 @@ def parse_args(pargs=None): parser.add_argument('--stddev-sample', required=False, action='store_true', help='Consider Bessels correction for stddeviation') - # Plot options + # 绘图选项 parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' diff --git a/samples/weekdays-filler/weekdaysfiller.py b/samples/weekdays-filler/weekdaysfiller.py index 2dce3da8f..50db83550 100644 --- a/samples/weekdays-filler/weekdaysfiller.py +++ b/samples/weekdays-filler/weekdaysfiller.py @@ -25,8 +25,8 @@ class WeekDaysFiller(object): - '''Bar Filler to add missing calendar days to trading days''' - # kickstart value for date comparisons + '''bar filler,用于为交易日补充缺失的工作日。''' + # 日期比较的初始值 ONEDAY = datetime.timedelta(days=1) lastdt = datetime.date.max - ONEDAY @@ -35,22 +35,20 @@ def __init__(self, data, fillclose=False): self.voidbar = [float('Nan')] * data.size() # init a void bar def __call__(self, data): - '''Empty bars (NaN) or with last close price are added for weekdays with no - data + '''为没有数据的工作日添加空 bar(NaN)或使用上一 close price 的 bar。 - Params: - - data: the data source to filter/process + Args: + - ``data``: 要过滤/处理的数据源。 Returns: - - True (always): bars are removed (even if put back on the stack) - + bool: 始终返回 ``True``;bars 会被移除,即便随后重新放回 stack。 ''' dt = data.datetime.date() # current date in int format lastdt = self.lastdt + self.ONEDAY # move last seen data once forward while lastdt < dt: # loop over gap bars if lastdt.isoweekday() < 6: # Mon-Fri - # Fill in date and add new bar to the stack + # 填充日期,并把新 bar 加入 stack if self.fillclose: self.voidbar = [self.lastclose] * data.size() dtime = datetime.datetime.combine(lastdt, data.p.sessionend) diff --git a/samples/writer-test/writer-test.py b/samples/writer-test/writer-test.py index c836f9a92..8df2cf9a8 100644 --- a/samples/writer-test/writer-test.py +++ b/samples/writer-test/writer-test.py @@ -24,7 +24,7 @@ import argparse import datetime -# The above could be sent to an independent module +# 上方内容可放到独立 module 中 import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind @@ -32,10 +32,9 @@ class LongShortStrategy(bt.Strategy): - '''This strategy buys/sells upong the close price crossing - upwards/downwards a Simple Moving Average. + '''根据 close price 与 Simple Moving Average 的上下交叉执行买卖。 - It can be a long-only strategy by setting the param "onlylong" to True + 将 ``onlylong`` 参数设为 ``True`` 时,可作为 long-only strategy 使用。 ''' params = dict( period=15, @@ -58,18 +57,18 @@ def log(self, txt, dt=None): print('%s, %s' % (dt.isoformat(), txt)) def __init__(self): - # To control operation entries + # 用于控制操作入口 self.orderid = None - # Create SMA on 2nd data + # 在第 2 个 data 上创建 SMA sma = btind.MovAv.SMA(self.data, period=self.p.period) - # Create a CrossOver Signal from close an moving average + # 从 close 和 moving average 创建 CrossOver Signal self.signal = btind.CrossOver(self.data.close, sma) self.signal.csv = self.p.csvcross def next(self): if self.orderid: - return # if an order is active, no new orders are allowed + return # 如果有 active order,则不允许新 orders if self.signal > 0.0: # cross upwards if self.position: @@ -104,7 +103,7 @@ def notify_order(self, order): self.log('%s ,' % order.Status[order.status]) pass # Simply log - # Allow new orders + # 允许新 orders self.orderid = None def notify_trade(self, trade): @@ -119,33 +118,33 @@ def notify_trade(self, trade): def runstrategy(): args = parse_args() - # Create a cerebro + # 创建 cerebro cerebro = bt.Cerebro() - # Get the dates from the args + # 从 args 获取日期 fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') - # Create the 1st data + # 创建第 1 个 data data = btfeeds.BacktraderCSVData( dataname=args.data, fromdate=fromdate, todate=todate) - # Add the 1st data to cerebro + # 添加第 1 个 data 到 cerebro cerebro.adddata(data) - # Add the strategy + # 添加 strategy cerebro.addstrategy(LongShortStrategy, period=args.period, onlylong=args.onlylong, csvcross=args.csvcross, stake=args.stake) - # Add the commission - only stocks like a for each operation + # 添加 commission;仅针对类似 stock 的每次操作 cerebro.broker.setcash(args.cash) - # Add the commission - only stocks like a for each operation + # 添加 commission;仅针对类似 stock 的每次操作 cerebro.broker.setcommission(commission=args.comm, mult=args.mult, margin=args.margin) @@ -154,10 +153,10 @@ def runstrategy(): cerebro.addwriter(bt.WriterFile, csv=args.writercsv, rounding=2) - # And run it + # 然后运行 cerebro.run() - # Plot if requested + # 按需绘图 if args.plot: cerebro.plot(numfigs=args.numfigs, volume=False, zdown=False) diff --git a/samples/yahoo-test/yahoo-test.py b/samples/yahoo-test/yahoo-test.py index 12e18f662..6422abccd 100644 --- a/samples/yahoo-test/yahoo-test.py +++ b/samples/yahoo-test/yahoo-test.py @@ -33,13 +33,13 @@ def runstrat(): args = parse_args() - # Create a cerebro entity + # 创建 cerebro 实体 cerebro = bt.Cerebro(stdstats=False) - # Add a strategy + # 添加 strategy cerebro.addstrategy(bt.Strategy) - # Get the dates from the args + # 从 args 获取日期 fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') @@ -48,20 +48,20 @@ def runstrat(): fromdate=fromdate, todate=todate) - # Add the resample data instead of the original + # 添加 resample data,而不是原始 data cerebro.adddata(data) - # Add a simple moving average if requirested + # 如果需要则添加 Simple Moving Average cerebro.addindicator(btind.SMA, period=args.period) - # Add a writer with CSV + # 添加带 CSV 的 writer if args.writer: cerebro.addwriter(bt.WriterFile, csv=args.wrcsv) - # Run over everything + # 运行全部流程 cerebro.run() - # Plot if requested + # 按需绘图 if args.plot: cerebro.plot(style='bar', numfigs=args.numfigs, volume=False) diff --git a/tests/test_analyzer-sqn.py b/tests/test_analyzer-sqn.py index f9efe4cbd..044aa3875 100644 --- a/tests/test_analyzer-sqn.py +++ b/tests/test_analyzer-sqn.py @@ -78,11 +78,11 @@ def notify_order(self, order): if self.p.printops: self.log('%s ,' % order.Status[order.status]) - # Allow new orders + # 允许新 orders self.orderid = None def __init__(self): - # Flag to allow new orders in the system or not + # 标记系统是否允许新 orders self.orderid = None self.sma = btind.SMA(self.data, period=self.p.period) @@ -126,7 +126,7 @@ def next(self): (self.data.close[0], self.sma[0])) if self.orderid: - # if an order is active, no new orders are allowed + # 如果有 active order,则不允许新 orders return if not self.position.size: @@ -176,7 +176,7 @@ def test_run(main=False): assert analysis.sqn == 0 assert analysis.trades == maxtrades else: - # Handle different precision + # 处理不同精度 assert str(analysis.sqn)[0:14] == '0.912550316439' assert str(analysis.trades) == '11' diff --git a/tests/test_analyzer-timereturn.py b/tests/test_analyzer-timereturn.py index 6c469b5df..21a4c1151 100644 --- a/tests/test_analyzer-timereturn.py +++ b/tests/test_analyzer-timereturn.py @@ -73,11 +73,11 @@ def notify_order(self, order): if self.p.printops: self.log('%s ,' % order.Status[order.status]) - # Allow new orders + # 允许新 orders self.orderid = None def __init__(self): - # Flag to allow new orders in the system or not + # 标记系统是否允许新 orders self.orderid = None self.sma = btind.SMA(self.data, period=self.p.period) @@ -120,7 +120,7 @@ def next(self): (self.data.close[0], self.sma[0])) if self.orderid: - # if an order is active, no new orders are allowed + # 如果有 active order,则不允许新 orders return if not self.position.size: @@ -164,7 +164,7 @@ def test_run(main=False): print(analysis) print(str(analysis[next(iter(analysis.keys()))])) else: - # Handle different precision + # 处理不同精度 if PY2: sval = '0.2795' else: diff --git a/tests/test_metaclass.py b/tests/test_metaclass.py index 3637e839d..36f25df33 100644 --- a/tests/test_metaclass.py +++ b/tests/test_metaclass.py @@ -21,19 +21,15 @@ import testcommon class TestFrompackages(testcommon.SampleParamsHolder): - """ - This class is used for testing that inheriting from base class that - uses `frompackages` import mechanism, doesnt brake the functionality - of the base class. - """ + """验证继承使用 ``frompackages`` import 机制的基类不会破坏基类功能。""" def __init__(self): super(TestFrompackages, self).__init__() - # Prepare the lags array + # 准备 lags array def test_run(main=False): - """ - Instantiate the TestFrompackages and see that no exception is raised - Bug Discussion: + """实例化 TestFrompackages,并验证不会抛出异常。 + + Bug 讨论: https://community.backtrader.com/topic/2661/frompackages-directive-functionality-seems-to-be-broken-when-using-inheritance """ test = TestFrompackages() diff --git a/tests/test_order.py b/tests/test_order.py index be895184b..0ff32a875 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -57,7 +57,7 @@ def close(self): def _execute(position, order, size, price, partial): - # Find position and do a real update - accounting happens here + # 找到 position 并执行真实更新;accounting 在这里发生 pprice_orig = position.price psize, pprice, opened, closed = position.update(size, price) @@ -97,7 +97,7 @@ def test_run(main=False): ### (Orders are cloned for each notification. The pending bits should be reported ### related to the previous notification (clone)) - # Add two bits and validate we have two pending bits + # 添加两个 bits,并验证存在两个 pending bits _execute(position, order, 10, 1.0, True) _execute(position, order, 20, 1.1, True) @@ -109,7 +109,7 @@ def test_run(main=False): assert pending[1].size == 20 assert pending[1].price == 1.1 - # Add additional two bits and validate we still have two pending bits after clone + # 再添加两个 bits,并验证 clone 后仍有两个 pending bits _execute(position, order, 30, 1.2, True) _execute(position, order, 40, 1.3, False) diff --git a/tests/test_strategy_optimized.py b/tests/test_strategy_optimized.py index 45f6acaca..3616cdf56 100644 --- a/tests/test_strategy_optimized.py +++ b/tests/test_strategy_optimized.py @@ -73,7 +73,7 @@ def log(self, txt, dt=None): print('%s, %s' % (dt.isoformat(), txt)) def __init__(self): - # Flag to allow new orders in the system or not + # 标记系统是否允许新 orders self.orderid = None self.sma = btind.SMA(self.data, period=self.p.period) @@ -104,7 +104,7 @@ def stop(self): def next(self): # print('self.data.close.array:', self.data.close.array) if self.orderid: - # if an order is active, no new orders are allowed + # 如果有 active order,则不允许新 orders return if not self.position.size: diff --git a/tests/test_strategy_unoptimized.py b/tests/test_strategy_unoptimized.py index 053a07fc1..9e18d431b 100644 --- a/tests/test_strategy_unoptimized.py +++ b/tests/test_strategy_unoptimized.py @@ -92,11 +92,11 @@ def notify_order(self, order): if self.p.printops: self.log('%s ,' % order.Status[order.status]) - # Allow new orders + # 允许新 orders self.orderid = None def __init__(self): - # Flag to allow new orders in the system or not + # 标记系统是否允许新 orders self.orderid = None self.sma = btind.SMA(self.data, period=self.p.period) @@ -159,7 +159,7 @@ def next(self): (self.data.close[0], self.sma[0])) if self.orderid: - # if an order is active, no new orders are allowed + # 如果有 active order,则不允许新 orders return if not self.position.size: diff --git a/tests/testcommon.py b/tests/testcommon.py index e052db28f..6239c6509 100644 --- a/tests/testcommon.py +++ b/tests/testcommon.py @@ -26,7 +26,7 @@ import os.path import sys -# append module root directory to sys.path +# 将 module 根目录加入 sys.path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import backtrader as bt @@ -224,10 +224,9 @@ def stop(self): class SampleParamsHolder(ParamsBase): - """ - This class is used as base for tests that check the proper - handling of meta parameters like `frompackages`, `packages`, `params`, `lines` - in inherited classes + """测试基类,用于检查继承类能否正确处理 meta parameters。 + + 涵盖 ``frompackages``、``packages``、``params`` 和 ``lines``。 """ frompackages = ( ('math', ('factorial')),