diff --git a/backtrader/analyzers/returns.py b/backtrader/analyzers/returns.py
index 7f426a376..074cb4d61 100644
--- a/backtrader/analyzers/returns.py
+++ b/backtrader/analyzers/returns.py
@@ -124,8 +124,17 @@ def stop(self):
self._value_end = self.strategy.broker.fundvalue
# Compound return
- self.rets['rtot'] = rtot = (
- math.log(self._value_end / self._value_start))
+ try:
+ nlrtot = self._value_end / self._value_start
+ except ZeroDivisionError:
+ rtot = float('-inf')
+ else:
+ if nlrtot < 0.0:
+ rtot = float('-inf')
+ else:
+ rtot = math.log(nlrtot)
+
+ self.rets['rtot'] = rtot
# Average return
self.rets['ravg'] = ravg = rtot / self._tcount
@@ -135,7 +144,11 @@ def stop(self):
if tann is None:
tann = self._TANN.get(self.data._timeframe, 1.0) # assign default
- self.rets['rnorm'] = rnorm = math.expm1(ravg * tann)
+ 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 %
def _on_dt_over(self):
diff --git a/backtrader/analyzers/tradeanalyzer.py b/backtrader/analyzers/tradeanalyzer.py
index 8d768318b..118a072b5 100644
--- a/backtrader/analyzers/tradeanalyzer.py
+++ b/backtrader/analyzers/tradeanalyzer.py
@@ -86,7 +86,7 @@ def notify_trade(self, trade):
# Trade just closed
won = res.won = int(trade.pnlcomm >= 0.0)
- lost = res.lost = int(won)
+ lost = res.lost = int(not won)
tlong = res.tlong = trade.long
tshort = res.tshort = not trade.long
diff --git a/backtrader/brokers/__init__.py b/backtrader/brokers/__init__.py
index df6e0dff9..b638c0d29 100644
--- a/backtrader/brokers/__init__.py
+++ b/backtrader/brokers/__init__.py
@@ -40,3 +40,8 @@
from .oandabroker import OandaBroker
except ImportError as e:
pass # The user may not have something installed
+
+try:
+ from .ccxtbroker import CCXTBroker
+except ImportError as e:
+ pass # The user may not have something installed
diff --git a/backtrader/brokers/ccxtbroker.py b/backtrader/brokers/ccxtbroker.py
new file mode 100644
index 000000000..9d125fc3e
--- /dev/null
+++ b/backtrader/brokers/ccxtbroker.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+# -*- coding: utf-8; py-indent-offset:4 -*-
+###############################################################################
+#
+# Copyright (C) 2015, 2016, 2017 Daniel Rodriguez
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+###############################################################################
+from __future__ import (absolute_import, division, print_function,
+ unicode_literals)
+
+from backtrader import BrokerBase, Order
+from backtrader.utils.py3 import queue
+from backtrader.stores.ccxtstore import CCXTStore, CCXTOrder
+
+class CCXTBroker(BrokerBase):
+ '''Broker implementation for CCXT cryptocurrency trading library.
+
+ This class maps the orders/positions from CCXT to the
+ internal API of ``backtrader``.
+ '''
+
+ order_types = {Order.Market: 'market',
+ Order.Limit: 'limit',
+ Order.Stop: 'stop',
+ Order.StopLimit: 'stop limit'}
+
+ def __init__(self, exchange, currency, config, retries=5):
+ super(CCXTBroker, self).__init__()
+
+ self.store = CCXTStore.get_store(exchange, config, retries)
+
+ self.currency = currency
+
+ self.notifs = queue.Queue() # holds orders which are notified
+
+ def getcash(self):
+ return self.store.getcash(self.currency)
+
+ def getvalue(self, datas=None):
+ return self.store.getvalue(self.currency)
+
+ def get_notification(self):
+ try:
+ return self.notifs.get(False)
+ except queue.Empty:
+ return None
+
+ def notify(self, order):
+ self.notifs.put(order)
+
+ def getposition(self, data):
+ currency = data.symbol.split('/')[0]
+ return self.store.getposition(currency)
+
+ def get_value(self, datas=None, mkt=False, lever=False):
+ return self.store.getvalue(self.currency)
+
+ def get_cash(self):
+ return self.store.getcash(self.currency)
+
+ def _submit(self, owner, data, exectype, side, amount, price, params):
+ order_type = self.order_types.get(exectype)
+ _order = self.store.create_order(symbol=data.symbol, order_type=order_type, side=side,
+ amount=amount, price=price, params=params)
+ order = CCXTOrder(owner, data, amount, _order)
+ self.notify(order)
+ return order
+
+ def buy(self, owner, data, size, price=None, plimit=None,
+ exectype=None, valid=None, tradeid=0, oco=None,
+ trailamount=None, trailpercent=None,
+ **kwargs):
+ return self._submit(owner, data, exectype, 'buy', size, price, kwargs)
+
+ def sell(self, owner, data, size, price=None, plimit=None,
+ exectype=None, valid=None, tradeid=0, oco=None,
+ trailamount=None, trailpercent=None,
+ **kwargs):
+ return self._submit(owner, data, exectype, 'sell', size, price, kwargs)
+
+ def cancel(self, order):
+ return self.store.cancel_order(order)
+
+ def get_orders_open(self, safe=False):
+ return self.store.fetch_open_orders()
diff --git a/backtrader/dataseries.py b/backtrader/dataseries.py
index 2770ffb27..a87df819c 100644
--- a/backtrader/dataseries.py
+++ b/backtrader/dataseries.py
@@ -40,7 +40,7 @@ class TimeFrame(object):
names = Names # support old naming convention
@classmethod
- def getname(cls, tframe, compression=None):
+ def getname(cls, tframe, compression=1):
tname = cls.Names[tframe]
if compression > 1 or tname == cls.Names[-1]:
return tname # for plural or 'NoTimeFrame' return plain entry
diff --git a/backtrader/feeds/__init__.py b/backtrader/feeds/__init__.py
index e39af5633..5c0da9550 100644
--- a/backtrader/feeds/__init__.py
+++ b/backtrader/feeds/__init__.py
@@ -52,3 +52,8 @@
from .rollover import RollOver
from .chainer import Chainer
+
+try:
+ from .ccxt import CCXT
+except ImportError:
+ pass # The user may not have something installed
diff --git a/backtrader/feeds/ccxt.py b/backtrader/feeds/ccxt.py
new file mode 100644
index 000000000..516ccc00a
--- /dev/null
+++ b/backtrader/feeds/ccxt.py
@@ -0,0 +1,192 @@
+#!/usr/bin/env python
+# -*- coding: utf-8; py-indent-offset:4 -*-
+###############################################################################
+#
+# Copyright (C) 2015, 2016, 2017 Daniel Rodriguez
+# Copyright (C) 2017 Ed Bartosh
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+###############################################################################
+from __future__ import (absolute_import, division, print_function,
+ unicode_literals)
+
+from collections import deque
+from datetime import datetime
+
+import backtrader as bt
+from backtrader.feed import DataBase
+from backtrader.stores.ccxtstore import CCXTStore
+
+class CCXT(DataBase):
+ """
+ CryptoCurrency eXchange Trading Library Data Feed.
+
+ Params:
+
+ - ``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.
+
+ - ``backfill_start`` (default: ``True``)
+
+ Perform backfilling at the start. The maximum possible historical data
+ will be fetched in a single request.
+ """
+
+ params = (
+ ('historical', False), # only historical download
+ ('backfill_start', False), # do backfilling at the start
+ )
+
+ # States for the Finite State Machine in _load
+ _ST_LIVE, _ST_HISTORBACK, _ST_OVER = range(3)
+
+ def __init__(self, exchange, symbol, ohlcv_limit=None, config={}, retries=5):
+ self.symbol = symbol
+ self.ohlcv_limit = ohlcv_limit
+
+ self.store = CCXTStore.get_store(exchange, config, retries)
+
+ self._data = deque() # data queue for price data
+ self._last_id = '' # last processed trade id for ohlcv
+ self._last_ts = 0 # last processed timestamp for ohlcv
+
+ def start(self, ):
+ DataBase.start(self)
+
+ if self.p.fromdate:
+ self._state = self._ST_HISTORBACK
+ self.put_notification(self.DELAYED)
+
+ self._fetch_ohlcv(self.p.fromdate)
+ else:
+ self._state = self._ST_LIVE
+ self.put_notification(self.LIVE)
+
+ def _load(self):
+ if self._state == self._ST_OVER:
+ return False
+
+ while True:
+ if self._state == self._ST_LIVE:
+ if self._timeframe == bt.TimeFrame.Ticks:
+ return self._load_ticks()
+ else:
+ self._fetch_ohlcv()
+ return self._load_ohlcv()
+ elif self._state == self._ST_HISTORBACK:
+ ret = self._load_ohlcv()
+ if ret:
+ return ret
+ else:
+ # End of historical data
+ if self.p.historical: # only historical
+ self.put_notification(self.DISCONNECTED)
+ self._state = self._ST_OVER
+ return False # end of historical
+ else:
+ self._state = self._ST_LIVE
+ self.put_notification(self.LIVE)
+ continue
+
+ def _fetch_ohlcv(self, fromdate=None):
+ """Fetch OHLCV data into self._data queue"""
+ granularity = self.store.get_granularity(self._timeframe, self._compression)
+
+ if fromdate:
+ since = int((fromdate - datetime(1970, 1, 1)).total_seconds() * 1000)
+ else:
+ if self._last_ts > 0:
+ since = self._last_ts
+ else:
+ since = None
+
+ limit = self.ohlcv_limit
+
+ while True:
+ dlen = len(self._data)
+ for ohlcv in sorted(self.store.fetch_ohlcv(self.symbol, timeframe=granularity,
+ since=since, limit=limit)):
+ if None in ohlcv:
+ continue
+
+ tstamp = ohlcv[0]
+ if tstamp > self._last_ts:
+ self._data.append(ohlcv)
+ self._last_ts = tstamp
+ since = tstamp + 1
+
+ if dlen == len(self._data):
+ break
+
+ def _load_ticks(self):
+ if self._last_id:
+ trades = self.store.fetch_trades(self.symbol)
+ else:
+ # first time get the latest trade only
+ trades = [self.store.fetch_trades(self.symbol)[-1]]
+
+ for trade in trades:
+ trade_id = trade['id']
+
+ if trade_id > self._last_id:
+ trade_time = datetime.strptime(trade['datetime'], '%Y-%m-%dT%H:%M:%S.%fZ')
+ self._data.append((trade_time, float(trade['price']), float(trade['amount'])))
+ self._last_id = trade_id
+
+ try:
+ trade = self._data.popleft()
+ except IndexError:
+ return None # no data in the queue
+
+ trade_time, price, size = trade
+
+ self.lines.datetime[0] = bt.date2num(trade_time)
+ self.lines.open[0] = price
+ self.lines.high[0] = price
+ self.lines.low[0] = price
+ self.lines.close[0] = price
+ self.lines.volume[0] = size
+
+ return True
+
+ def _load_ohlcv(self):
+ try:
+ ohlcv = self._data.popleft()
+ except IndexError:
+ return None # no data in the queue
+
+ tstamp, open_, high, low, close, volume = ohlcv
+
+ dtime = datetime.utcfromtimestamp(tstamp // 1000)
+
+ self.lines.datetime[0] = bt.date2num(dtime)
+ self.lines.open[0] = open_
+ self.lines.high[0] = high
+ self.lines.low[0] = low
+ self.lines.close[0] = close
+ self.lines.volume[0] = volume
+
+ return True
+
+ def haslivedata(self):
+ return self._state == self._ST_LIVE and self._data
+
+ def islive(self):
+ return not self.p.historical
diff --git a/backtrader/feeds/rollover.py b/backtrader/feeds/rollover.py
index bd00e108e..2d64b0822 100644
--- a/backtrader/feeds/rollover.py
+++ b/backtrader/feeds/rollover.py
@@ -152,7 +152,7 @@ def _checkcondition(self, d0, d1):
def _load(self):
while self._d is not None:
- if not self._d.next(): # no values from current data source
+ if self._d.next() is not False: # no values from current data src
if self._ds:
self._d = self._ds.pop(0)
self._dts.pop(0)
diff --git a/backtrader/plot/locator.py b/backtrader/plot/locator.py
index e9cf745b8..ccf070562 100644
--- a/backtrader/plot/locator.py
+++ b/backtrader/plot/locator.py
@@ -27,6 +27,7 @@
'''
import datetime
+import warnings
from matplotlib.dates import AutoDateLocator as ADLocator
from matplotlib.dates import RRuleLocator as RRLocator
@@ -36,7 +37,7 @@
MONTHS_PER_YEAR, DAYS_PER_WEEK,
SEC_PER_HOUR, SEC_PER_DAY,
num2date, rrulewrapper, YearLocator,
- MicrosecondLocator, warnings)
+ MicrosecondLocator)
from dateutil.relativedelta import relativedelta
import numpy as np
diff --git a/backtrader/resamplerfilter.py b/backtrader/resamplerfilter.py
index e94a5aa54..c4cec0d21 100644
--- a/backtrader/resamplerfilter.py
+++ b/backtrader/resamplerfilter.py
@@ -111,7 +111,7 @@ class _BaseResampler(with_metaclass(metabase.MetaParams, object)):
def __init__(self, data):
self.subdays = TimeFrame.Ticks < self.p.timeframe < TimeFrame.Days
self.subweeks = self.p.timeframe < TimeFrame.Weeks
- self.componly = (self.subdays and
+ self.componly = (not self.subdays and
data._timeframe == self.p.timeframe and
not (self.p.compression % data._compression)
)
@@ -524,7 +524,6 @@ def __call__(self, data, fromcheck=False, forcedata=None):
if consumed:
self.bar.bupdate(data) # update new or existing bar
- data.backwards() # remove used bar
# if self.bar.isopen and (onedge or (docheckover and checkbarover))
cond = self.bar.isopen()
@@ -533,6 +532,10 @@ def __call__(self, data, fromcheck=False, forcedata=None):
if docheckover:
cond = self._checkbarover(data, fromcheck=fromcheck,
forcedata=forcedata)
+
+ if consumed:
+ data.backwards() # remove used bar
+
if cond:
dodeliver = False
if forcedata is not None:
diff --git a/backtrader/stores/ccxtstore.py b/backtrader/stores/ccxtstore.py
new file mode 100644
index 000000000..466305e7d
--- /dev/null
+++ b/backtrader/stores/ccxtstore.py
@@ -0,0 +1,161 @@
+#!/usr/bin/env python
+# -*- coding: utf-8; py-indent-offset:4 -*-
+###############################################################################
+#
+# Copyright (C) 2017 Ed Bartosh
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+###############################################################################
+from __future__ import (absolute_import, division, print_function,
+ unicode_literals)
+
+import time
+from functools import wraps
+
+import ccxt
+from ccxt.base.errors import NetworkError, ExchangeError
+
+import backtrader as bt
+from backtrader import OrderBase
+
+class CCXTOrder(OrderBase):
+ def __init__(self, owner, data, size, ccxt_order):
+ self.owner = owner
+ self.data = data
+ self.ccxt_order = ccxt_order
+ self.ordtype = self.Buy if ccxt_order['side'] == 'buy' else self.Sell
+ amount = ccxt_order.get('amount')
+ if amount:
+ self.size = float(amount)
+ else:
+ self.size = size
+
+ super(CCXTOrder, self).__init__()
+
+class CCXTStore(object):
+ '''API provider for CCXT feed and broker classes.'''
+
+ # Supported granularities
+ _GRANULARITIES = {
+ (bt.TimeFrame.Minutes, 1): '1m',
+ (bt.TimeFrame.Minutes, 3): '3m',
+ (bt.TimeFrame.Minutes, 5): '5m',
+ (bt.TimeFrame.Minutes, 15): '15m',
+ (bt.TimeFrame.Minutes, 30): '30m',
+ (bt.TimeFrame.Minutes, 60): '1h',
+ (bt.TimeFrame.Minutes, 90): '90m',
+ (bt.TimeFrame.Minutes, 120): '2h',
+ (bt.TimeFrame.Minutes, 240): '4h',
+ (bt.TimeFrame.Minutes, 360): '6h',
+ (bt.TimeFrame.Minutes, 480): '8h',
+ (bt.TimeFrame.Minutes, 720): '12h',
+ (bt.TimeFrame.Days, 1): '1d',
+ (bt.TimeFrame.Days, 3): '3d',
+ (bt.TimeFrame.Weeks, 1): '1w',
+ (bt.TimeFrame.Weeks, 2): '2w',
+ (bt.TimeFrame.Months, 1): '1M',
+ (bt.TimeFrame.Months, 3): '3M',
+ (bt.TimeFrame.Months, 6): '6M',
+ (bt.TimeFrame.Years, 1): '1y',
+ }
+
+ # exchange -> store dictionary to track already initialized exchanges
+ stores = {}
+ # exchange -> config dictionry
+ configs = {}
+
+ @classmethod
+ def get_store(cls, exchange, config, retries):
+ store = cls.stores.get(exchange)
+ if store:
+ store_conf = cls.configs[exchange]
+ if store_conf:
+ if not set(config.items()).issubset(set(store_conf.items())):
+ raise ValueError("%s exchange is already configured: %s" % \
+ (exchange, store_conf))
+ return store
+
+ cls.configs[exchange] = config
+ cls.stores[exchange] = cls(exchange, config, retries)
+
+ return cls.stores[exchange]
+
+ def __init__(self, exchange, config, retries):
+ self.exchange = getattr(ccxt, exchange)(config)
+ self.retries = retries
+
+ def get_granularity(self, timeframe, compression):
+ if not self.exchange.has['fetchOHLCV']:
+ raise NotImplementedError("'%s' exchange doesn't support fetching OHLCV data" % \
+ self.exchange.name)
+
+ granularity = self._GRANULARITIES.get((timeframe, compression))
+ if granularity is None:
+ raise ValueError("backtrader CCXT module doesn't support fetching OHLCV "
+ "data for time frame %s, comression %s" % \
+ (bt.TimeFrame.getname(timeframe), compression))
+
+ if self.exchange.timeframes and granularity not in self.exchange.timeframes:
+ raise ValueError("'%s' exchange doesn't support fetching OHLCV data for "
+ "%s time frame" % (self.exchange.name, granularity))
+
+ return granularity
+
+ def retry(method):
+ @wraps(method)
+ def retry_method(self, *args, **kwargs):
+ for i in range(self.retries):
+ time.sleep(self.exchange.rateLimit / 1000)
+ try:
+ return method(self, *args, **kwargs)
+ except (NetworkError, ExchangeError):
+ if i == self.retries - 1:
+ raise
+
+ return retry_method
+
+ @retry
+ def getcash(self, currency):
+ return self.exchange.fetch_balance()['free'].get(currency, 0.0)
+
+ @retry
+ def getvalue(self, currency):
+ return self.exchange.fetch_balance()['total'].get(currency, 0.0)
+
+ @retry
+ def getposition(self, currency):
+ return self.getvalue(currency)
+
+ @retry
+ def create_order(self, symbol, order_type, side, amount, price, params):
+ order = self.exchange.create_order(symbol=symbol, type=order_type, side=side,
+ amount=amount, price=price, params=params)
+ return self.exchange.parse_order(order['info'])
+
+ @retry
+ def cancel_order(self, order):
+ return self.exchange.cancel_order(order.ccxt_order['id'])
+
+ @retry
+ def fetch_trades(self, symbol):
+ return self.exchange.fetch_trades(symbol)
+
+ @retry
+ def fetch_ohlcv(self, symbol, timeframe, since, limit):
+ return self.exchange.fetch_ohlcv(symbol, timeframe=timeframe, since=since, limit=limit)
+
+ @retry
+ def fetch_open_orders(self):
+ return self.exchange.fetchOpenOrders()
diff --git a/backtrader/utils/dateintern.py b/backtrader/utils/dateintern.py
index cfcd30be5..bcced1f53 100644
--- a/backtrader/utils/dateintern.py
+++ b/backtrader/utils/dateintern.py
@@ -206,7 +206,7 @@ def date2num(dt, tz=None):
is a :func:`float`.
"""
if tz is not None:
- dt = tz.localize(dwhen)
+ dt = tz.localize(tz)
if hasattr(dt, 'tzinfo') and dt.tzinfo is not None:
delta = dt.tzinfo.utcoffset(dt)
diff --git a/backtrader/version.py b/backtrader/version.py
index 982541b94..a0a22dd3e 100644
--- a/backtrader/version.py
+++ b/backtrader/version.py
@@ -22,6 +22,6 @@
unicode_literals)
-__version__ = '1.9.65.122'
+__version__ = '1.9.66.122'
__btversion__ = tuple(int(x) for x in __version__.split('.'))
diff --git a/changelog.txt b/changelog.txt
index 57692b4b2..fe5b591df 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,12 @@
+1.9.66.122
+ - Fix regression introduced with 8f537a1c2c271eb5cfc592b373697732597d26d6
+ which voids the count of lost trades
+ - Allow rollover to distinguish between no values temporarily (with None)
+ and no values permanently (with False)
+ - Avoid math domain error for negative returns in logarithmic calculations
+ - Fix local variable declaration for compound returns
+ - Fix typo in date2num tz conversion which shows up in direct usage
+
1.9.65.122
- Fix commission info assigment and orderref seeking in OandaStore (PR#367)
- Add strategy type to OptReturn (PR#364)