Skip to content

Commit

Permalink
Merge branch 'make_initial_fetch_balance_optional_for_feed'
Browse files Browse the repository at this point in the history
  • Loading branch information
Dave-Vallance committed Mar 14, 2019
2 parents 06156b5 + 0874158 commit 1918e81
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 53 deletions.
48 changes: 25 additions & 23 deletions ccxtbt/ccxtbroker.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@
###############################################################################
from __future__ import (absolute_import, division, print_function,
unicode_literals)

import collections
from backtrader.position import Position
import json

from backtrader import BrokerBase, OrderBase, Order
from backtrader.position import Position
from backtrader.utils.py3 import queue, with_metaclass

from .ccxtstore import CCXTStore
import json


class CCXTOrder(OrderBase):
def __init__(self, owner, data, ccxt_order):
Expand All @@ -37,13 +41,15 @@ def __init__(self, owner, data, ccxt_order):

super(CCXTOrder, self).__init__()


class MetaCCXTBroker(BrokerBase.__class__):
def __init__(cls, name, bases, dct):
'''Class has already been created ... register'''
# Initialize the class
super(MetaCCXTBroker, cls).__init__(name, bases, dct)
CCXTStore.BrokerCls = cls


class CCXTBroker(with_metaclass(MetaCCXTBroker, BrokerBase)):
'''Broker implementation for CCXT cryptocurrency trading library.
This class maps the orders/positions from CCXT to the
Expand Down Expand Up @@ -90,48 +96,46 @@ class CCXTBroker(with_metaclass(MetaCCXTBroker, BrokerBase)):

order_types = {Order.Market: 'market',
Order.Limit: 'limit',
Order.Stop: 'stop', #stop-loss for kraken, stop for bitmex
Order.Stop: 'stop', # stop-loss for kraken, stop for bitmex
Order.StopLimit: 'stop limit'}

mappings = {
'closed_order':{
'closed_order': {
'key': 'status',
'value':'closed'
},
'canceled_order':{
'value': 'closed'
},
'canceled_order': {
'key': 'status',
'value':'canceled'}
}

'value': 'canceled'}
}

def __init__(self, broker_mapping=None, debug=False, **kwargs):
super(CCXTBroker, self).__init__()

if broker_mapping is not None:
try:
self.order_types = broker_mapping['order_types']
except KeyError: # Might not want to change the order types
except KeyError: # Might not want to change the order types
pass
try:
self.mappings = broker_mapping['mappings']
except KeyError: # might not want to change the mappings
except KeyError: # might not want to change the mappings
pass


self.store = CCXTStore(**kwargs)

self.currency = self.store.currency

self.positions = collections.defaultdict(Position)

self.debug = debug
self.indent = 4 # For pretty printing dictionaries
self.indent = 4 # For pretty printing dictionaries

self.notifs = queue.Queue() # holds orders which are notified

self.open_orders = list()

self.startingcash = self.store._cash
self.startingcash = self.store._cash
self.startingvalue = self.store._value

def get_balance(self):
Expand All @@ -149,12 +153,12 @@ def get_wallet_balance(self, currency, params={}):
def getcash(self):
# Get cash seems to always be called before get value
# Therefore it makes sense to add getbalance here.
#return self.store.getcash(self.currency)
# return self.store.getcash(self.currency)
self.cash = self.store._cash
return self.cash

def getvalue(self, datas=None):
#return self.store.getvalue(self.currency)
# return self.store.getvalue(self.currency)
self.value = self.store._value
return self.value

Expand Down Expand Up @@ -208,7 +212,7 @@ def _submit(self, owner, data, exectype, side, amount, price, params):
params = params['params'] if 'params' in params else params

ret_ord = self.store.create_order(symbol=data.symbol, order_type=order_type, side=side,
amount=amount, price=price, params=params)
amount=amount, price=price, params=params)

_order = self.store.fetch_order(ret_ord['id'], data.symbol)

Expand Down Expand Up @@ -242,15 +246,13 @@ def cancel(self, order):
print('Broker cancel() called')
print('Fetching Order ID: {}'.format(oID))


# check first if the order has already been filled otherwise an error
# might be raised if we try to cancel an order that is not open.
ccxt_order = self.store.fetch_order(oID, order.data.symbol)

if self.debug:
print(json.dumps(ccxt_order, indent=self.indent))


if ccxt_order[self.mappings['closed_order']['key']] == self.mappings['closed_order']['value']:
return order

Expand Down Expand Up @@ -287,9 +289,9 @@ def private_end_point(self, type, endpoint, params):
print(dir(ccxt.hitbtc()))
'''
endpoint_str = endpoint.replace('/','_')
endpoint_str = endpoint_str.replace('{','')
endpoint_str = endpoint_str.replace('}','')
endpoint_str = endpoint.replace('/', '_')
endpoint_str = endpoint_str.replace('{', '')
endpoint_str = endpoint_str.replace('}', '')

method_str = 'private_' + type.lower() + endpoint_str.lower()

Expand Down
36 changes: 19 additions & 17 deletions ccxtbt/ccxtfeed.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@
from __future__ import (absolute_import, division, print_function,
unicode_literals)

import time
from collections import deque
from datetime import datetime

import backtrader as bt
from backtrader.feed import DataBase
from backtrader.utils.py3 import with_metaclass
from backtrader.metabase import MetaParams

from .ccxtstore import CCXTStore
import time


class MetaCCXTFeed(DataBase.__class__):
Expand Down Expand Up @@ -78,14 +79,14 @@ class CCXTFeed(with_metaclass(MetaCCXTFeed, DataBase)):
# 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):
# def __init__(self, exchange, symbol, ohlcv_limit=None, config={}, retries=5):
def __init__(self, **kwargs):
self.symbol = self.p.dataname
# self.store = CCXTStore(exchange, config, retries)
self.store = self._store(**kwargs)
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
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)
Expand All @@ -99,7 +100,6 @@ def start(self, ):
self._state = self._ST_LIVE
self.put_notification(self.LIVE)


def _load(self):
if self._state == self._ST_OVER:
return False
Expand Down Expand Up @@ -148,26 +148,28 @@ def _fetch_ohlcv(self, fromdate=None):
while True:
dlen = len(self._data)


if self.p.debug:
# TESTING
since_dt = datetime.utcfromtimestamp(since // 1000) if since is not None else 'NA'
print('---- NEW REQUEST ----')
print('{} - Requesting: Since TS {} Since date {} granularity {}, limit {}, params'.format(datetime.utcnow(),since, since_dt, granularity, limit, self.p.fetch_ohlcv_params))
print('{} - Requesting: Since TS {} Since date {} granularity {}, limit {}, params'.format(
datetime.utcnow(), since, since_dt, granularity, limit, self.p.fetch_ohlcv_params))
data = sorted(self.store.fetch_ohlcv(self.symbol, timeframe=granularity,
since=since, limit=limit, params=self.p.fetch_ohlcv_params))
since=since, limit=limit, params=self.p.fetch_ohlcv_params))
try:
for i, ohlcv in enumerate(data):
tstamp, open_, high, low, close, volume = ohlcv
print('{} - Data {}: {} - TS {} Time {}'.format(datetime.utcnow(), i, datetime.utcfromtimestamp(tstamp // 1000), tstamp, (time.time() * 1000)))
print('{} - Data {}: {} - TS {} Time {}'.format(datetime.utcnow(), i,
datetime.utcfromtimestamp(tstamp // 1000),
tstamp, (time.time() * 1000)))
# ------------------------------------------------------------------
except IndexError:
print('Index Error: Data = {}'.format(data))
print('---- REQUEST END ----')
else:

data = sorted(self.store.fetch_ohlcv(self.symbol, timeframe=granularity,
since=since, limit=limit, params=self.p.fetch_ohlcv_params))
since=since, limit=limit, params=self.p.fetch_ohlcv_params))

# Check to see if dropping the latest candle will help with
# exchanges which return partial data
Expand All @@ -176,16 +178,16 @@ def _fetch_ohlcv(self, fromdate=None):

for ohlcv in data:

#for ohlcv in sorted(self.store.fetch_ohlcv(self.symbol, timeframe=granularity,
# since=since, limit=limit, params=self.p.fetch_ohlcv_params)):
# for ohlcv in sorted(self.store.fetch_ohlcv(self.symbol, timeframe=granularity,
# since=since, limit=limit, params=self.p.fetch_ohlcv_params)):

if None in ohlcv:
continue

tstamp = ohlcv[0]

# Prevent from loading incomplete data
#if tstamp > (time.time() * 1000):
# if tstamp > (time.time() * 1000):
# continue

if tstamp > self._last_ts:
Expand Down Expand Up @@ -215,7 +217,7 @@ def _load_ticks(self):
try:
trade = self._data.popleft()
except IndexError:
return None # no data in the queue
return None # no data in the queue

trade_time, price, size = trade

Expand All @@ -232,7 +234,7 @@ def _load_ohlcv(self):
try:
ohlcv = self._data.popleft()
except IndexError:
return None # no data in the queue
return None # no data in the queue

tstamp, open_, high, low, close, volume = ohlcv

Expand Down
25 changes: 12 additions & 13 deletions ccxtbt/ccxtstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,19 @@
unicode_literals)

import time
from datetime import datetime
from functools import wraps

import ccxt
from ccxt.base.errors import NetworkError, ExchangeError
from datetime import datetime
import backtrader as bt
import ccxt
from backtrader.metabase import MetaParams
from backtrader.utils.py3 import queue, with_metaclass
import json
from backtrader.utils.py3 import with_metaclass
from ccxt.base.errors import NetworkError, ExchangeError


class MetaSingleton(MetaParams):
'''Metaclass to make a metaclassed class a singleton'''

def __init__(cls, name, bases, dct):
super(MetaSingleton, cls).__init__(name, bases, dct)
cls._singleton = None
Expand Down Expand Up @@ -94,15 +95,14 @@ def getbroker(cls, *args, **kwargs):
'''Returns broker with *args, **kwargs from registered ``BrokerCls``'''
return cls.BrokerCls(*args, **kwargs)

def __init__(self, exchange, currency, config, retries, broker_delay=None, debug=False):
def __init__(self, exchange, currency, config, retries, debug=False):
self.exchange = getattr(ccxt, exchange)(config)
self.currency = currency
self.retries = retries
self.debug = debug

balance = self.exchange.fetch_balance()
self._cash = balance['free'][currency]
self._value = balance['total'][currency]
balance = self.exchange.fetch_balance() if 'secret' in config else 0
self._cash = 0 if balance == 0 else balance['free'][currency]
self._value = 0 if balance == 0 else balance['total'][currency]

def get_granularity(self, timeframe, compression):
if not self.exchange.has['fetchOHLCV']:
Expand Down Expand Up @@ -136,7 +136,6 @@ def retry_method(self, *args, **kwargs):

return retry_method


@retry
def get_wallet_balance(self, currency, params=None):
balance = self.exchange.fetch_balance(params)
Expand All @@ -151,7 +150,7 @@ def get_balance(self):
@retry
def getposition(self):
return self._value
#return self.getvalue(currency)
# return self.getvalue(currency)

@retry
def create_order(self, symbol, order_type, side, amount, price, params):
Expand All @@ -170,7 +169,7 @@ def fetch_trades(self, symbol):
@retry
def fetch_ohlcv(self, symbol, timeframe, since, limit, params={}):
if self.debug:
print('Fetching: {}, TF: {}, Since: {}, Limit: {}'.format(symbol,timeframe,since,limit))
print('Fetching: {}, TF: {}, Since: {}, Limit: {}'.format(symbol, timeframe, since, limit))
return self.exchange.fetch_ohlcv(symbol, timeframe=timeframe, since=since, limit=limit, params=params)

@retry
Expand Down
42 changes: 42 additions & 0 deletions samples/backtesting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import time
from datetime import datetime

import backtrader as bt

from ccxtbt import CCXTFeed


def main():
class TestStrategy(bt.Strategy):
def __init__(self):
self.next_runs = 0

def next(self, dt=None):
dt = dt or self.datas[0].datetime.datetime(0)
print('%s closing price: %s' % (dt.isoformat(), self.datas[0].close[0]))
self.next_runs += 1

cerebro = bt.Cerebro()

cerebro.addstrategy(TestStrategy)

# Add the feed
cerebro.adddata(CCXTFeed(exchange='binance',
dataname='BNB/USDT',
timeframe=bt.TimeFrame.Minutes,
fromdate=datetime(2019, 1, 1, 0, 0),
todate=datetime(2019, 1, 1, 0, 2),
compression=1,
ohlcv_limit=2,
currency='BNB',
retries=5,

# 'apiKey' and 'secret' are skipped
config={'enableRateLimit': True, 'nonce': lambda: str(int(time.time() * 1000))}))

# Run the strategy
cerebro.run()


if __name__ == '__main__':
main()
Empty file added test/__init__.py
Empty file.
1 change: 1 addition & 0 deletions test/ccxtbt/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Loading

0 comments on commit 1918e81

Please sign in to comment.