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