Repository: lindomar-oliveira/backtrader-binance Branch: main Commit: 695db65f8289 Files: 10 Total size: 19.0 KB Directory structure: gitextract_x0l9znb4/ ├── .gitignore ├── LICENSE ├── README.md ├── backtrader_binance/ │ ├── __init__.py │ ├── binance_broker.py │ ├── binance_feed.py │ └── binance_store.py ├── examples/ │ └── live_trade.py ├── requirements.txt └── setup.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ __pycache__/ .idea/ *.egg-info/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Lindomar Oliveira Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # backtrader-binance Create your strategy for the [Backtrader](https://www.backtrader.com), do the backtesting and you will also be ready for live trading on the exchange [Binance](https://www.binance.com/pt-PT/register?ref=D9K8QI13) with this integration. Installation ============ pip install git+https://github.com/lindomar-oliveira/backtrader-binance.git@main Usage ===== See examples folder Thanks ====== - backtrader: An incredible library... - [python-binance](https://github.com/sammchardy/python-binance): For creating Binance API wrapper, shortening a lot of work. License ======= [MIT](https://choosealicense.com/licenses/mit) ================================================ FILE: backtrader_binance/__init__.py ================================================ from .binance_store import BinanceStore ================================================ FILE: backtrader_binance/binance_broker.py ================================================ import datetime as dt from collections import defaultdict, deque from math import copysign from backtrader.broker import BrokerBase from backtrader.order import Order, OrderBase from backtrader.position import Position from binance.enums import * class BinanceOrder(OrderBase): def __init__(self, owner, data, exectype, binance_order): self.owner = owner self.data = data self.exectype = exectype self.ordtype = self.Buy if binance_order['side'] == SIDE_BUY else self.Sell # Market order price is zero if self.exectype == Order.Market: self.size = float(binance_order['executedQty']) self.price = sum(float(fill['price']) for fill in binance_order['fills']) / len(binance_order['fills']) # Average price else: self.size = float(binance_order['origQty']) self.price = float(binance_order['price']) self.binance_order = binance_order super(BinanceOrder, self).__init__() self.accept() class BinanceBroker(BrokerBase): _ORDER_TYPES = { Order.Limit: ORDER_TYPE_LIMIT, Order.Market: ORDER_TYPE_MARKET, Order.Stop: ORDER_TYPE_STOP_LOSS, Order.StopLimit: ORDER_TYPE_STOP_LOSS_LIMIT, } def __init__(self, store): super(BinanceBroker, self).__init__() self.notifs = deque() self.positions = defaultdict(Position) self.open_orders = list() self._store = store self._store.binance_socket.start_user_socket(self._handle_user_socket_message) def _execute_order(self, order, date, executed_size, executed_price): order.execute( date, executed_size, executed_price, 0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0.0) pos = self.getposition(order.data, clone=False) pos.update(copysign(executed_size, order.size), executed_price) def _handle_user_socket_message(self, msg): """https://binance-docs.github.io/apidocs/spot/en/#payload-order-update""" if msg['e'] == 'executionReport': if msg['s'] == self._store.symbol: for o in self.open_orders: if o.binance_order['orderId'] == msg['i']: if msg['X'] in [ORDER_STATUS_FILLED, ORDER_STATUS_PARTIALLY_FILLED]: date = dt.datetime.fromtimestamp(msg['T'] / 1000) executed_size = float(msg['l']) executed_price = float(msg['L']) self._execute_order(o, dt, executed_size, executed_price) self._set_order_status(o, msg['X']) if o.status not in [Order.Accepted, Order.Partial]: self.open_orders.remove(o) self.notify(o) elif msg['e'] == 'error': raise msg def _set_order_status(self, order, binance_order_status): if binance_order_status == ORDER_STATUS_CANCELED: order.cancel() elif binance_order_status == ORDER_STATUS_EXPIRED: order.expire() elif binance_order_status == ORDER_STATUS_FILLED: order.completed() elif binance_order_status == ORDER_STATUS_PARTIALLY_FILLED: order.partial() elif binance_order_status == ORDER_STATUS_REJECTED: order.reject() def _submit(self, owner, data, side, exectype, size, price): type = self._ORDER_TYPES.get(exectype, ORDER_TYPE_MARKET) binance_order = self._store.create_order(side, type, size, price) order = BinanceOrder(owner, data, exectype, binance_order) if binance_order['status'] in [ORDER_STATUS_FILLED, ORDER_STATUS_PARTIALLY_FILLED]: self._execute_order( order, dt.datetime.fromtimestamp(binance_order['transactTime'] / 1000), float(binance_order['executedQty']), float(binance_order['price'])) self._set_order_status(order, binance_order['status']) if order.status == Order.Accepted: self.open_orders.append(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, SIDE_BUY, exectype, size, price) def cancel(self, order): order_id = order.binance_order['orderId'] self._store.cancel_order(order_id) def format_price(self, value): return self._store.format_price(value) def get_asset_balance(self, asset): return self._store.get_asset_balance(asset) def getcash(self): self.cash = self._store._cash return self.cash def get_notification(self): if not self.notifs: return None return self.notifs.popleft() def getposition(self, data, clone=True): pos = self.positions[data._dataname] if clone: pos = pos.clone() return pos def getvalue(self, datas=None): self.value = self._store._value return self.value def notify(self, order): self.notifs.append(order) 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, SIDE_SELL, exectype, size, price) ================================================ FILE: backtrader_binance/binance_feed.py ================================================ from collections import deque import pandas as pd from backtrader.dataseries import TimeFrame from backtrader.feed import DataBase from backtrader.utils import date2num class BinanceData(DataBase): params = ( ('drop_newest', True), ) # States for the Finite State Machine in _load _ST_LIVE, _ST_HISTORBACK, _ST_OVER = range(3) def __init__(self, store, timeframe_in_minutes, start_date=None): self.timeframe_in_minutes = timeframe_in_minutes self.start_date = start_date self._store = store self._data = deque() def _handle_kline_socket_message(self, msg): """https://binance-docs.github.io/apidocs/spot/en/#kline-candlestick-streams""" if msg['e'] == 'kline': if msg['k']['x']: # Is closed kline = self._parser_to_kline(msg['k']['t'], msg['k']) self._data.extend(kline.values.tolist()) elif msg['e'] == 'error': raise msg def _load(self): if self._state == self._ST_OVER: return False elif self._state == self._ST_LIVE: return self._load_kline() elif self._state == self._ST_HISTORBACK: if self._load_kline(): return True else: self._start_live() def _load_kline(self): try: kline = self._data.popleft() except IndexError: return None timestamp, open_, high, low, close, volume = kline self.lines.datetime[0] = date2num(timestamp) 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 _parser_dataframe(self, data): df = data.copy() df.columns = ['timestamp', 'open', 'high', 'low', 'close', 'volume'] df['timestamp'] = df['timestamp'].values.astype(dtype='datetime64[ms]') df['open'] = df['open'].values.astype(float) df['high'] = df['high'].values.astype(float) df['low'] = df['low'].values.astype(float) df['close'] = df['close'].values.astype(float) df['volume'] = df['volume'].values.astype(float) # df.set_index('timestamp', inplace=True) return df def _parser_to_kline(self, timestamp, kline): df = pd.DataFrame([[timestamp, kline['o'], kline['h'], kline['l'], kline['c'], kline['v']]]) return self._parser_dataframe(df) def _start_live(self): self._state = self._ST_LIVE self.put_notification(self.LIVE) self._store.binance_socket.start_kline_socket( self._handle_kline_socket_message, self.symbol_info['symbol'], self.interval) def haslivedata(self): return self._state == self._ST_LIVE and self._data def islive(self): return True def start(self): DataBase.start(self) self.interval = self._store.get_interval(TimeFrame.Minutes, self.timeframe_in_minutes) if self.interval is None: self._state = self._ST_OVER self.put_notification(self.NOTSUPPORTED_TF) return self.symbol_info = self._store.get_symbol_info(self._store.symbol) if self.symbol_info is None: self._state = self._ST_OVER self.put_notification(self.NOTSUBSCRIBED) return if self.start_date: self._state = self._ST_HISTORBACK self.put_notification(self.DELAYED) klines = self._store.binance.get_historical_klines( self.symbol_info['symbol'], self.interval, self.start_date.strftime('%d %b %Y %H:%M:%S')) if self.p.drop_newest: klines.pop() df = pd.DataFrame(klines) df.drop(df.columns[[6, 7, 8, 9, 10, 11]], axis=1, inplace=True) # Remove unnecessary columns df = self._parser_dataframe(df) self._data.extend(df.values.tolist()) else: self._start_live() ================================================ FILE: backtrader_binance/binance_store.py ================================================ import time from functools import wraps from math import floor from backtrader.dataseries import TimeFrame from binance import Client, ThreadedWebsocketManager from binance.enums import * from binance.exceptions import BinanceAPIException from requests.exceptions import ConnectTimeout, ConnectionError from .binance_broker import BinanceBroker from .binance_feed import BinanceData class BinanceStore(object): _GRANULARITIES = { (TimeFrame.Minutes, 1): KLINE_INTERVAL_1MINUTE, (TimeFrame.Minutes, 3): KLINE_INTERVAL_3MINUTE, (TimeFrame.Minutes, 5): KLINE_INTERVAL_5MINUTE, (TimeFrame.Minutes, 15): KLINE_INTERVAL_15MINUTE, (TimeFrame.Minutes, 30): KLINE_INTERVAL_30MINUTE, (TimeFrame.Minutes, 60): KLINE_INTERVAL_1HOUR, (TimeFrame.Minutes, 120): KLINE_INTERVAL_2HOUR, (TimeFrame.Minutes, 240): KLINE_INTERVAL_4HOUR, (TimeFrame.Minutes, 360): KLINE_INTERVAL_6HOUR, (TimeFrame.Minutes, 480): KLINE_INTERVAL_8HOUR, (TimeFrame.Minutes, 720): KLINE_INTERVAL_12HOUR, (TimeFrame.Days, 1): KLINE_INTERVAL_1DAY, (TimeFrame.Days, 3): KLINE_INTERVAL_3DAY, (TimeFrame.Weeks, 1): KLINE_INTERVAL_1WEEK, (TimeFrame.Months, 1): KLINE_INTERVAL_1MONTH, } def __init__(self, api_key, api_secret, coin_refer, coin_target, testnet=False, retries=5): self.binance = Client(api_key, api_secret, testnet=testnet) self.binance_socket = ThreadedWebsocketManager(api_key, api_secret, testnet=testnet) self.binance_socket.daemon = True self.binance_socket.start() self.coin_refer = coin_refer self.coin_target = coin_target self.symbol = coin_refer + coin_target self.retries = retries self._cash = 0 self._value = 0 self.get_balance() self._step_size = None self._tick_size = None self.get_filters() self._broker = BinanceBroker(store=self) self._data = None def _format_value(self, value, step): precision = step.find('1') - 1 if precision > 0: return '{:0.0{}f}'.format(value, precision) return floor(int(value)) def retry(func): @wraps(func) def wrapper(self, *args, **kwargs): for attempt in range(1, self.retries + 1): time.sleep(60 / 1200) # API Rate Limit try: return func(self, *args, **kwargs) except (BinanceAPIException, ConnectTimeout, ConnectionError) as err: if isinstance(err, BinanceAPIException) and err.code == -1021: # Recalculate timestamp offset between local and Binance's server res = self.binance.get_server_time() self.binance.timestamp_offset = res['serverTime'] - int(time.time() * 1000) if attempt == self.retries: raise return wrapper @retry def cancel_open_orders(self): orders = self.binance.get_open_orders(symbol=self.symbol) if len(orders) > 0: self.binance._request_api('delete', 'openOrders', signed=True, data={ 'symbol': self.symbol }) @retry def cancel_order(self, order_id): try: self.binance.cancel_order(symbol=self.symbol, orderId=order_id) except BinanceAPIException as api_err: if api_err.code == -2011: # Order filled return else: raise api_err except Exception as err: raise err @retry def create_order(self, side, type, size, price): params = dict() if type in [ORDER_TYPE_LIMIT, ORDER_TYPE_STOP_LOSS_LIMIT]: params.update({ 'timeInForce': TIME_IN_FORCE_GTC }) if type != ORDER_TYPE_MARKET: params.update({ 'price': self.format_price(price) }) return self.binance.create_order( symbol=self.symbol, side=side, type=type, quantity=self.format_quantity(size), **params) def format_price(self, price): return self._format_value(price, self._tick_size) def format_quantity(self, size): return self._format_value(size, self._step_size) @retry def get_asset_balance(self, asset): balance = self.binance.get_asset_balance(asset) return float(balance['free']), float(balance['locked']) def get_balance(self): free, locked = self.get_asset_balance(self.coin_target) self._cash = free self._value = free + locked def getbroker(self): return self._broker def getdata(self, timeframe_in_minutes, start_date=None): if not self._data: self._data = BinanceData(store=self, timeframe_in_minutes=timeframe_in_minutes, start_date=start_date) return self._data def get_filters(self): symbol_info = self.get_symbol_info(self.symbol) for f in symbol_info['filters']: if f['filterType'] == 'LOT_SIZE': self._step_size = f['stepSize'] elif f['filterType'] == 'PRICE_FILTER': self._tick_size = f['tickSize'] def get_interval(self, timeframe, compression): return self._GRANULARITIES.get((timeframe, compression)) @retry def get_symbol_info(self, symbol): return self.binance.get_symbol_info(symbol) def stop_socket(self): self.binance_socket.stop() self.binance_socket.join(5) ================================================ FILE: examples/live_trade.py ================================================ import datetime as dt import backtrader as bt from backtrader_binance import BinanceStore class RSIStrategy(bt.Strategy): def __init__(self): self.rsi = bt.indicators.RSI(period=14) # RSI indicator def next(self): print('Open: {}, High: {}, Low: {}, Close: {}'.format( self.data.open[0], self.data.high[0], self.data.low[0], self.data.close[0])) print('RSI: {}'.format(self.rsi[0])) if not self.position: if self.rsi < 30: # Enter long self.buy() else: if self.rsi > 70: self.sell() # Close long position def notify_order(self, order): print(order) if __name__ == '__main__': cerebro = bt.Cerebro(quicknotify=True) store = BinanceStore( api_key='YOUR_BINANCE_KEY', api_secret='YOUR_BINANCE_SECRET', coin_refer='BTC', coin_target='USDT', testnet=True) broker = store.getbroker() cerebro.setbroker(broker) from_date = dt.datetime.utcnow() - dt.timedelta(minutes=5*16) data = store.getdata( timeframe_in_minutes=5, start_date=from_date) cerebro.addstrategy(RSIStrategy) cerebro.adddata(data) cerebro.run() ================================================ FILE: requirements.txt ================================================ backtrader==1.9.76.123 matplotlib==3.2.2 pandas==1.2.4 python-binance==1.0.12 ================================================ FILE: setup.py ================================================ import os from setuptools import setup with open(os.path.join('README.md')) as desc: LONG_DESCRIPTION = desc.read() with open(os.path.join('requirements.txt')) as reqs: REQUIREMENTS = reqs.readlines() setup( name='backtrader-binance', version='1.0.0', description='Binance API integration with backtrader', long_description=LONG_DESCRIPTION, long_description_content_type='text/markdown', url='https://github.com/lindomar-oliveira/backtrader-binance', author='Lindomar Oliveira', author_email='lindomar.souza1999@gmail.com', license='MIT', packages=['backtrader_binance'], python_requires='>=3.7', keywords='backtrader,binance,bitcoin,bot,crypto,trading', install_requires=REQUIREMENTS )