Repository: MohammedRashad/Crypto-Copy-Trader Branch: master Commit: 26016c5d8d99 Files: 29 Total size: 76.7 KB Directory structure: gitextract_wz8roz6m/ ├── .gitignore ├── Actions/ │ └── Actions.py ├── Dockerfile ├── ExchangeInterfaces/ │ ├── BinanceExchange.py │ ├── BitmexExchange.py │ ├── BitmexTest.py │ └── Exchange.py ├── Helpers/ │ ├── Bitmex_websocket_mod.py │ ├── Helpers.py │ └── Order.py ├── LICENSE ├── README.md ├── SlaveContainer.py ├── api.py ├── changelog.md ├── config_files/ │ ├── config-sample.json │ ├── config.csv │ ├── symbols.csv │ ├── symbols2.csv │ ├── symbols3.csv │ └── symbols4.csv ├── logs/ │ └── cct.log ├── requirements.txt ├── static/ │ └── styles/ │ └── base.css └── templates/ ├── home.html ├── includes/ │ ├── _formhelpers.html │ ├── _messages.html │ └── _navbar.html └── layout.html ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ dist dist-* cabal-dev *.o *.hi *.chi *.chs.h *.dyn_o *.dyn_hi .hpc .hsenv .cabal-sandbox/ cabal.sandbox.config *.prof *.aux *.hp *.eventlog .stack-work/ cabal.project.local cabal.project.local~ .HTF/ .ghc.environment.* config_files/config.json ================================================ FILE: Actions/Actions.py ================================================ import inspect from abc import ABC class Action(ABC): name = 'abstract_action' def __init__(self, exchange, original_event): self.exchange = exchange self.original_event = original_event def __str__(self): # print all attributes attributes = {} for i in inspect.getmembers(self): # Ignores anything starting with underscore # (that is, private and protected attributes) if not i[0].startswith('_'): # Ignores methods if not inspect.ismethod(i[1]) and not i[0] == 'original_event': attributes[i[0]] = i[1] return str(attributes) class ActionNewOrder(Action): name = 'new_order' def __init__(self, order, exchange, original_event): super().__init__(exchange, original_event) self.order = order class ActionCancel(Action): name = 'cancel' def __init__(self, symbol, price, order_id, exchange, original_event): super().__init__(exchange, original_event) self.symbol = symbol self.price = price self.order_id = order_id class ActionClosePosition(Action): name = 'close_position' def __init__(self, symbol, order_type, price, order_id, exchange, original_event): super().__init__(exchange, original_event) self.symbol = symbol self.price = price self.order_id = order_id self.order_type = order_type ================================================ FILE: Dockerfile ================================================ FROM phusion/baseimage:latest # Use baseimage-docker's init system. CMD ["/sbin/my_init"] RUN apt-get update -y && \ apt-get install -y python3-pip python3-dev python-dev RUN apt-get install -y build-essential autoconf libtool pkg-config python-opengl python-imaging python-pyrex python-pyside.qtopengl idle-python2.7 qt4-dev-tools qt4-designer libqtgui4 libqtcore4 libqt4-xml libqt4-test libqt4-script libqt4-network libqt4-dbus python-qt4 python-qt4-gl libgle3 python-dev libssl-dev RUN pip3 install --no-cache-dir Cython # We copy just the requirements.txt first to leverage Docker cache COPY ./requirements.txt /app/requirements.txt WORKDIR /app RUN pip3 install --no-cache-dir Cython RUN pip3 install -r requirements.txt COPY . /app EXPOSE 5000 ENTRYPOINT [ "python3" ] CMD [ "api.py" ] ================================================ FILE: ExchangeInterfaces/BinanceExchange.py ================================================ import math import Actions.Actions as Actions from binance.exceptions import BinanceAPIException from .Exchange import Exchange from binance.client import Client from binance.websockets import BinanceSocketManager from Helpers.Order import Order class BinanceExchange(Exchange): exchange_name = "Binance" isMargin = False def __init__(self, apiKey, apiSecret, pairs, name): super().__init__(apiKey, apiSecret, pairs, name) self.connection = Client(self.api['key'], self.api['secret']) symbol_info_arr = self.connection.get_exchange_info() dict_symbols_info = {item['symbol']: item for item in symbol_info_arr["symbols"]} actual_symbols_info = {symbol: dict_symbols_info[symbol] for symbol in self.pairs} self.symbols_info = actual_symbols_info self.update_balance() self.socket = BinanceSocketManager(self.connection) self.socket.start_user_socket(self.on_balance_update) self.socket.start() self.is_last_order_event_completed = True self.step_sizes = {} self.balance_updated = True for symbol_info in symbol_info_arr['symbols']: if symbol_info['symbol'] in self.pairs: self.step_sizes[symbol_info['symbol']] = \ [f['stepSize'] for f in symbol_info['filters'] if f['filterType'] == 'LOT_SIZE'][0] def start(self, caller_callback): self.socket.start_user_socket(caller_callback) def update_balance(self): account_information = self.connection.get_account() self.set_balance(account_information['balances']) def get_trading_symbols(self): symbols = set() if not self.symbols_info: raise RuntimeError("Cant get exchange info") for key, value in self.symbols_info.items(): symbols.add(value["quoteAsset"]) symbols.add(value["baseAsset"]) return symbols def set_balance(self, balances): symbols = self.get_trading_symbols() dict_balances = {item['asset']: item for item in balances} actual_balance = {symbol: dict_balances[symbol] for symbol in symbols} self.balance = actual_balance def on_balance_update(self, upd_balance_ev): if upd_balance_ev['e'] == 'outboundAccountPosition': balance = [] for ev in upd_balance_ev['B']: balance.append({'asset': ev['a'], 'free': ev['f'], 'locked': ev['l']}) self.balance.update({item['asset']: item for item in balance}) def get_open_orders(self): orders = self.connection.get_open_orders() general_orders = [] for o in orders: quantityPart = self.get_part(o['symbol'], o["origQty"], o['price'], o['side']) general_orders.append( Order(o['price'], o["origQty"], quantityPart, o['orderId'], o['symbol'], o['side'], o['type'], self.exchange_name)) return general_orders def _cancel_order(self, order_id, symbol): self.connection.cancel_order(symbol=symbol, orderId=order_id) self.logger.info(f'{self.name}: Order canceled') async def on_cancel_handler(self, event: Actions.ActionCancel): try: slave_order_id = self._cancel_order_detector(event.price) self._cancel_order(slave_order_id, event.symbol) except BinanceAPIException as error: self.logger.error(f'{self.name}: error {error.message}') except: self.logger.error(f"{self.name}: error in action: {event.name} in slave {self.name}") def stop(self): self.socket.close() def _cancel_order_detector(self, price): # detect order id which need to be canceled slave_open_orders = self.connection.get_open_orders() for ordr_open in slave_open_orders: if float(ordr_open['price']) == float(price): return ordr_open['orderId'] def process_event(self, event): # return event in generic type from websocket # if this event in general type it was send from start function and need call firs_copy if 'exchange' in event: return event if event['e'] == 'outboundAccountPosition': self.on_balance_update(event) elif event['e'] == 'executionReport': if event['X'] == 'FILLED': return elif event['x'] == 'CANCELED': return Actions.ActionCancel( event['s'], event['p'], event['i'], self.exchange_name, event ) elif event['X'] == 'NEW': order_event = event if order_event['s'] not in self.pairs: return if order_event['o'] == 'MARKET': # if market order, we haven't price and cant calculate quantity order_event['p'] = self.connection.get_ticker(symbol=order_event['s'])['lastPrice'] # part = self.get_part(order_event['s'], order_event['q'], order_event['p'], order_event['S']) # shortcut mean https://github.com/binance-exchange/binance-official-api-docs/blob/master/user-data-stream.md#order-update order = Order(order_event['p'], order_event['q'], self.get_part(order_event['s'], order_event['q'], order_event['p'], order_event['S']), order_event['i'], order_event['s'], order_event['S'], order_event['o'], self.exchange_name, order_event['P']) return Actions.ActionNewOrder(order, self.exchange_name, event) return async def on_order_handler(self, event: Actions.ActionNewOrder): self.create_order(event.order) def create_order(self, order): """ :param order: """ quantity = self.calc_quantity_from_part(order.symbol, order.quantityPart, order.price, order.side) self.logger.info('Slave ' + self.name + ' ' + str(self._get_quote_balance(order.symbol)) + ' ' + str(self._get_base_balance(order.symbol)) + ', Create Order:' + ' amount: ' + str(quantity) + ', price: ' + str(order.price)) try: if order.type == 'STOP_LOSS_LIMIT' or order.type == "TAKE_PROFIT_LIMIT": self.connection.create_order(symbol=order.symbol, side=order.side, type=order.type, price=order.price, quantity=quantity, timeInForce='GTC', stopPrice=order.stop) if order.type == 'MARKET': self.connection.create_order(symbol=order.symbol, side=order.side, type=order.type, quantity=quantity) else: self.connection.create_order(symbol=order.symbol, side=order.side, type=order.type, quantity=quantity, price=order.price, timeInForce='GTC') self.logger.info(f"{self.name}: order created") except Exception as e: self.logger.error(str(e)) def _get_quote_balance(self, symbol): return self.balance[self.symbols_info[symbol]['quoteAsset']] def _get_base_balance(self, symbol): return self.balance[self.symbols_info[symbol]['baseAsset']] def get_part(self, symbol: str, quantity: float, price: float, side: str): # get part of the total balance of this coin # if order[side] == sell: need obtain coin balance if side == 'BUY': get_context_balance = self._get_quote_balance market_value = float(quantity) * float(price) else: get_context_balance = self._get_base_balance market_value = float(quantity) balance = float(get_context_balance(symbol)['free']) # if first_copy the balance was update before if self.balance_updated: balance += float(get_context_balance(symbol)['locked']) # else: # balance += market_value part = market_value / balance part = part * 0.99 # decrease part for 1% for avoid rounding errors in calculation return part def calc_quantity_from_part(self, symbol, quantityPart, price, side): # calculate quantity from quantityPart # if order[side] == sell: need obtain coin balance if side == 'BUY': get_context_balance = self._get_quote_balance buy_koef = float(price) else: get_context_balance = self._get_base_balance buy_koef = 1 cur_bal = float(get_context_balance(symbol)['free']) if self.balance_updated: cur_bal += float(get_context_balance(symbol)['locked']) quantity = quantityPart * cur_bal / buy_koef stepSize = float(self.step_sizes[symbol]) precision = int(round(-math.log(stepSize, 10), 0)) quantity = round(quantity, precision) return quantity ================================================ FILE: ExchangeInterfaces/BitmexExchange.py ================================================ from .Exchange import Exchange # from bitmex_websocket import BitMEXWebsocket from Helpers.Bitmex_websocket_mod import BitMEXWebsocket_mod as BitMEXWebsocket import bitmex from Helpers.Order import Order import Actions.Actions as Actions # TEST_BITMEX_URL = "wss://testnet.bitmex.com" # BITMEX_URL = "wss://www.bitmex.com" class BitmexExchange(Exchange): exchange_name = "Bitmex" isMargin = True ENDPOINT = "https://www.bitmex.com/api/v1" TEST = False def __init__(self, apiKey, apiSecret, pairs, name): super().__init__(apiKey, apiSecret, pairs, name) self.pairs = list(map(lambda pair: self.translate(pair) if pair != self.translate(pair) else self.logger.debug(f"Can't translate word {pair} in {self.exchange_name}"), self.pairs)) self.pairs = list(filter(None, self.pairs)) self.connection = bitmex.bitmex(api_key=apiKey, api_secret=apiSecret, test=self.TEST) self.socket = {} # self.firts_copy_flag = True self.balance_updated = False for pair in self.pairs: if pair == 'XBTUSD': self.socket['XBTUSD'] = BitMEXWebsocket(endpoint=self.ENDPOINT, symbol='XBTUSD', api_key=self.api['key'], api_secret=self.api['secret'], on_balance_update=self.on_balance_update) else: self.socket[pair] = BitMEXWebsocket(endpoint=self.ENDPOINT, symbol=pair, api_key=self.api['key'], api_secret=self.api['secret'] ) def start(self, caller_callback): self.stop() self.socket['XBTUSD'] = BitMEXWebsocket(endpoint=self.ENDPOINT, symbol='XBTUSD', api_key=self.api['key'], api_secret=self.api['secret'], on_balance_update=self.on_balance_update, on_order_calback=caller_callback) for pair in self.pairs: if pair == 'XBTUSD': continue self.socket[pair] = BitMEXWebsocket(endpoint=self.ENDPOINT, symbol=pair, api_key=self.api['key'], api_secret=self.api['secret'], on_order_calback=caller_callback, ) def stop(self): for socket in self.socket: self.socket[socket].exit() def on_balance_update(self, event): if 'availableMargin' in event: self.balance = event['availableMargin'] / (10**8) * (self.socket['XBTUSD'].get_instrument()['midPrice']\ if "XBTUSD" in self.socket else self.connection.Instrument.Instrument_get(symbol='XBTUSD', count=1, reverse=True).result()[0][0]['midPrice']) self.balance_updated = True def update_balance(self): self.balance = self.socket['XBTUSD'].funds()['availableMargin'] / 10**8 *\ self.socket['XBTUSD'].get_instrument()['midPrice'] def get_open_orders(self): open_orders = [] for pair in self.pairs: open_orders += self.socket[pair].open_orders(clOrdIDPrefix="") general_orders = [] for o in open_orders: general_orders.append(self._self_order_to_global(o)) return general_orders def get_part(self, symbol, quantity: float, price: float): # btc = float(quantity) / float(price) # btc_satoshi = btc * (10 ** 8) usd_order_value = quantity balance = self.get_balance() # if self.balance_updated: # part = usd_order_value / float(balance + usd_order_value) # else: # part = usd_order_value / balance part = usd_order_value / balance part = part * 0.99 # decrease part for 1% for avoid rounding errors in calculation return part def calc_quantity_from_part(self, symbol, quantityPart, price, **kwargs): amount_usd = float(quantityPart) * float(self.get_balance()) # btc = btc_satoshi / (10 ** 8) # amount_usd = float(btc) * float(price) return amount_usd def process_event(self, event): # self.update_balance() self.balance_updated = False if event['action'] == "insert": check_result = self.check_expected_order(event) if check_result: return check_result data = event['data'][0] if data['ordStatus'] == 'New' \ or (data['ordStatus'] == 'Filled' and 'ordType' in data): if event['data'][0]['execInst'] == 'Close': # order to close position came close_order = event['data'][0] if close_order['ordType'] == 'Market': price = None else: price = close_order['price'] return Actions.ActionClosePosition( self.translate(close_order['symbol']), self.translate(close_order['ordType']), price, close_order['orderID'], self.exchange_name, event ) else: order = self._self_order_to_global(event['data'][0]) return Actions.ActionNewOrder( order, self.exchange_name, event) elif event['action'] == 'update': if 'ordStatus' not in event['data'][0]: return if event['data'][0]['ordStatus'] == 'Canceled': orders = self.socket[event['data'][0]['symbol']].open_orders(clOrdIDPrefix="") order = list(filter(lambda o: o['orderID'] == event['data'][0]['orderID'], orders))[0] global_order = self._self_order_to_global(order) return Actions.ActionCancel( global_order.symbol, global_order.price, global_order.id, self.exchange_name, event ) async def on_order_handler(self, event: Actions.ActionNewOrder): self.create_order(event.order) async def on_cancel_handler(self, event: Actions.ActionCancel): if self.is_program_order(event.order_id): order_id = None clOrderId = event.order_id else: order_id = self._cancel_order_detector(event.price) clOrderId = None if order_id or clOrderId: self._cancel_order(order_id, clOrderId) else: self.logger.error(f'Cancel rejected: Cant find necessary order in slave {self.name}') def _self_order_to_global(self, o) -> Order: if 'stopPx' not in o: o['stopPx'] = 0 if o['price'] is None: o['price'] = self.socket[o['symbol']].get_instrument()['midPrice'] return Order(o['price'], o["orderQty"], self.get_part(o['symbol'], o["orderQty"], self.socket['XBTUSD'].get_instrument()['midPrice']), o['orderID'], self.translate(o['symbol']), o['side'].upper(), self.translate(o['ordType']), self.exchange_name, stop=o['stopPx']) def _cancel_order_detector(self, price): # detect order id which need to be canceled open_orders = self.get_open_orders() for ordr_open in open_orders: if ordr_open.price == price: return ordr_open.id def _cancel_order(self, order_id, clOrderID=None): try: if clOrderID: result = self.connection.Order.Order_cancel(clOrdID=clOrderID).result() else: result = self.connection.Order.Order_cancel(orderID=order_id).result() self.logger.info(f'{self.name}: Cancel order request send. Response: {result}') self.logger.info(f'{self.name}: Order canceled') except: self.logger.exception(f'{self.name}: Error cancel order') def create_order(self, order: Order): try: quantity = self.calc_quantity_from_part(order.symbol, order.quantityPart, self.socket['XBTUSD'].get_instrument()['midPrice']) self.logger.info(f"Slave {self.name}, balance: {self.get_balance()}; " f"Create Order: amount {quantity}, price: {order.price} ") self.ids.append(order.id) if order.type == 'MARKET' or order.type == 'Stop' or order.type == 'MarketIfTouched': new_order = self.connection.Order.Order_new(symbol=self.translate(order.symbol), side=self.translate(order.side), orderQty=quantity, stopPx=order.stop, ordType=self.translate(order.type), clOrdID=order.id, timeInForce='GoodTillCancel') else: new_order = self.connection.Order.Order_new(symbol=self.translate(order.symbol), side=self.translate(order.side), orderQty=quantity, price=order.price, stopPx=order.stop, clOrdID=order.id, ordType=self.translate(order.type), timeInForce='GoodTillCancel' ) self.logger.info(f'{self.name} Create order request send') self.logger.debug(f'Response: {new_order.result()} ') except: self.logger.exception(f'{self.name}: Error create order') async def close_position(self, event: Actions.ActionClosePosition): self.logger.info(f'{self.name}: close_position {event.symbol}') if event.order_type == 'MARKET': return self.connection.Order.Order_new(symbol=self.translate(event.symbol), ordType='Market', execInst='Close').result() else: self.ids.append(event.order_id) return self.connection.Order.Order_new(symbol=self.translate(event.symbol), ordType='Limit', price=event.price, execInst='Close', clOrdID=event.order_id).result() translate_dict = { 'BTCUSDT': 'XBTUSD', 'ETHUSDT': 'ETHUSD', 'LIMIT': 'Limit', 'STOP_LOSS_LIMIT': 'StopLimit', 'MARKET': 'Market', 'BUY': 'Buy', 'SELL': 'Sell' # 'BCHUSDT': 'BCHUSD', # 'TRXUSDT': 'TRXU20', # 'XRPUSDT': 'XRPUSD' } @staticmethod def translate(word) -> str: translate_dict = BitmexExchange.translate_dict if not word in translate_dict: translate_dict = dict(zip(BitmexExchange.translate_dict.values(), BitmexExchange.translate_dict.keys())) if not word in translate_dict: return word return translate_dict[word] ================================================ FILE: ExchangeInterfaces/BitmexTest.py ================================================ from ExchangeInterfaces.BitmexExchange import BitmexExchange class BitmexTest(BitmexExchange): ENDPOINT = "https://testnet.bitmex.com/api/v1" TEST = True ================================================ FILE: ExchangeInterfaces/Exchange.py ================================================ from abc import ABC, abstractmethod import logging import Actions.Actions as Actions class Exchange(ABC): exchange_name = None isMargin = None def __init__(self, apiKey, apiSecret, pairs, name): self.api = {'key': apiKey, 'secret': apiSecret} # delete '\n' from symbols' self.pairs = list(map(lambda pair: pair.replace('\n', ''), pairs)) self.name = name self.balance = None self.expected_orders = list() self.ids = [] # store here order which was created by program self.logger = logging.getLogger('cct') def get_balance(self) -> float: return self.balance @abstractmethod def stop(self): pass @abstractmethod def start(self, caller_callback): pass @abstractmethod def process_event(self, event) -> Actions.Action or None: pass @abstractmethod def on_order_handler(self, event: Actions.ActionNewOrder): pass @abstractmethod def get_open_orders(self): pass @abstractmethod async def on_cancel_handler(self, event: Actions.ActionCancel): pass @abstractmethod def create_order(self, order): pass async def async_create_order(self, order): self.create_order(order) @abstractmethod def get_part(self, symbol: str, quantity: float, price: float) -> float: pass @abstractmethod def calc_quantity_from_part(self, symbol: str, quantityPart: float, price: float, side:str): pass def add_expected_order_id(self, id, callback): self.expected_orders.append({'id': id, 'callback': callback}) def check_expected_order(self, order): for expected_order in self.expected_orders: if order.id == expected_order['id']: expected_order['callback'](order) async def close_position(self, event: Actions.ActionClosePosition): print(f" exchange {self.exchange_name} do not support event \' close_position \' ") def is_program_order(self, _id) -> bool: if _id in self.ids: return True return False def delete_id(self, _id): self.ids.remove(_id) ================================================ FILE: Helpers/Bitmex_websocket_mod.py ================================================ import websocket import threading import traceback from time import sleep import json import logging import urllib import math from util.api_key import generate_nonce, generate_signature # from bitmex_websocket import BitMEXWebsocket # , find_by_keys, order_leaves_quantity # the copy of class from BitMEXWebsocket but with modified: # methods __init__() and __on_message() # for using callback class BitMEXWebsocket_mod: # Don't grow a table larger than this amount. Helps cap memory usage. MAX_TABLE_LEN = 200 def __init__(self, endpoint, symbol, api_key=None, api_secret=None, on_order_calback=None, on_balance_update=None): '''Connect to the websocket and initialize data stores.''' self.logger = logging.getLogger(__name__) self.logger.debug("Initializing WebSocket.") self.endpoint = endpoint self.symbol = symbol self.on_order_callback = on_order_calback self.on_balance_update= on_balance_update if api_key is not None and api_secret is None: raise ValueError('api_secret is required if api_key is provided') if api_key is None and api_secret is not None: raise ValueError('api_key is required if api_secret is provided') self.api_key = api_key self.api_secret = api_secret self.data = {} self.keys = {} self.exited = False # We can subscribe right in the connection querystring, so let's build that. # Subscribe to all pertinent endpoints wsURL = self.__get_url() self.logger.info("Connecting to %s" % wsURL) self.__connect(wsURL, symbol) self.logger.info('Connected to WS.') # Connected. Wait for partials self.__wait_for_symbol(symbol) if api_key: self.__wait_for_account() self.logger.info('Got all market data. Starting.') def exit(self): '''Call this to exit - will close websocket.''' self.exited = True self.ws.close() def get_instrument(self): '''Get the raw instrument data for this symbol.''' # Turn the 'tickSize' into 'tickLog' for use in rounding instrument = self.data['instrument'][0] instrument['tickLog'] = int(math.fabs(math.log10(instrument['tickSize']))) return instrument def get_ticker(self): '''Return a ticker object. Generated from quote and trade.''' lastQuote = self.data['quote'][-1] lastTrade = self.data['trade'][-1] ticker = { "last": lastTrade['price'], "buy": lastQuote['bidPrice'], "sell": lastQuote['askPrice'], "mid": (float(lastQuote['bidPrice'] or 0) + float(lastQuote['askPrice'] or 0)) / 2 } # The instrument has a tickSize. Use it to round values. instrument = self.data['instrument'][0] return {k: round(float(v or 0), instrument['tickLog']) for k, v in ticker.items()} def funds(self): '''Get your margin details.''' return self.data['margin'][0] def positions(self): '''Get your positions.''' return self.data['position'] def market_depth(self): '''Get market depth (orderbook). Returns all levels.''' return self.data['orderBookL2'] def open_orders(self, clOrdIDPrefix): '''Get all your open orders.''' orders = self.data['order'] # Filter to only open orders and those that we actually placed return [o for o in orders if str(o['clOrdID']).startswith(clOrdIDPrefix) and order_leaves_quantity(o)] def recent_trades(self): '''Get recent trades.''' return self.data['trade'] # # End Public Methods # def __connect(self, wsURL, symbol): '''Connect to the websocket in a thread.''' self.logger.debug("Starting thread") self.ws = websocket.WebSocketApp(wsURL, on_message=self.__on_message, on_close=self.__on_close, on_open=self.__on_open, on_error=self.__on_error, header=self.__get_auth()) self.wst = threading.Thread(target=lambda: self.ws.run_forever()) self.wst.daemon = True self.wst.start() self.logger.debug("Started thread") # Wait for connect before continuing conn_timeout = 5 while not self.ws.sock or not self.ws.sock.connected and conn_timeout: sleep(1) conn_timeout -= 1 if not conn_timeout: self.logger.error("Couldn't connect to WS! Exiting.") self.exit() raise websocket.WebSocketTimeoutException('Couldn\'t connect to WS! Exiting.') def __get_auth(self): '''Return auth headers. Will use API Keys if present in settings.''' if self.api_key: self.logger.info("Authenticating with API Key.") # To auth to the WS using an API key, we generate a signature of a nonce and # the WS API endpoint. expires = generate_nonce() return [ "api-expires: " + str(expires), "api-signature: " + generate_signature(self.api_secret, 'GET', '/realtime', expires, ''), "api-key:" + self.api_key ] else: self.logger.info("Not authenticating.") return [] def __get_url(self): ''' Generate a connection URL. We can define subscriptions right in the querystring. Most subscription topics are scoped by the symbol we're listening to. ''' # You can sub to orderBookL2 for all levels, or orderBook10 for top 10 levels & save bandwidth symbolSubs = ["execution", "instrument", "order", "orderBookL2", "position", "quote", "trade"] genericSubs = ["margin"] subscriptions = [sub + ':' + self.symbol for sub in symbolSubs] subscriptions += genericSubs urlParts = list(urllib.parse.urlparse(self.endpoint)) urlParts[0] = urlParts[0].replace('http', 'ws') urlParts[2] = "/realtime?subscribe={}".format(','.join(subscriptions)) return urllib.parse.urlunparse(urlParts) def __wait_for_account(self): '''On subscribe, this data will come down. Wait for it.''' # Wait for the keys to show up from the ws while not {'margin', 'position', 'order', 'orderBookL2'} <= set(self.data): sleep(0.1) def __wait_for_symbol(self, symbol): '''On subscribe, this data will come down. Wait for it.''' while not {'instrument', 'trade', 'quote'} <= set(self.data): sleep(0.1) def __send_command(self, command, args=None): '''Send a raw command.''' if args is None: args = [] self.ws.send(json.dumps({"op": command, "args": args})) def __on_message(self, message): '''Handler for parsing WS messages.''' message = json.loads(message) self.logger.debug(json.dumps(message)) table = message.get("table") action = message.get("action") try: if 'subscribe' in message: self.logger.debug("Subscribed to %s." % message['subscribe']) elif action: if table not in self.data: self.data[table] = [] if (self.on_order_callback != None) and table == "order": self.on_order_callback(message) if(self.on_balance_update!= None) and table == "margin": self.on_balance_update(message['data'][0]) # There are four possible actions from the WS: # 'partial' - full table image # 'insert' - new row # 'update' - update row # 'delete' - delete row if action == 'partial': self.logger.debug("%s: partial" % table) self.data[table] = message['data'] # Keys are communicated on partials to let you know how to uniquely identify # an item. We use it for updates. self.keys[table] = message['keys'] elif action == 'insert': self.logger.debug('%s: inserting %s' % (table, message['data'])) self.data[table] += message['data'] # Limit the max length of the table to avoid excessive memory usage. # Don't trim orders because we'll lose valuable state if we do. if table not in ['order', 'orderBookL2'] and len(self.data[table]) > BitMEXWebsocket_mod.MAX_TABLE_LEN: self.data[table] = self.data[table][BitMEXWebsocket_mod.MAX_TABLE_LEN // 2:] elif action == 'update': self.logger.debug('%s: updating %s' % (table, message['data'])) # Locate the item in the collection and update it. for updateData in message['data']: item = find_by_keys(self.keys[table], self.data[table], updateData) if not item: return # No item found to update. Could happen before push item.update(updateData) # Remove cancelled / filled orders if table == 'order' and not order_leaves_quantity(item): self.data[table].remove(item) elif action == 'delete': self.logger.debug('%s: deleting %s' % (table, message['data'])) # Locate the item in the collection and remove it. for deleteData in message['data']: item = find_by_keys(self.keys[table], self.data[table], deleteData) self.data[table].remove(item) else: raise Exception("Unknown action: %s" % action) except: self.logger.error(traceback.format_exc()) def __on_error(self, error): '''Called on fatal websocket errors. We exit on these.''' if not self.exited: self.logger.error("Bitmex Websocket Error : %s" % error) # try to fix Error connection is already closed self.__init__(self.endpoint, self.symbol, self.api_key, self.api_secret, on_order_calback=self.on_order_callback, on_balance_update=self.on_balance_update) # raise websocket.WebSocketException(error) def __on_open(self): '''Called when the WS opens.''' self.logger.debug("Websocket Opened.") def __on_close(self): '''Called on websocket close.''' self.logger.info('Websocket Closed') # Utility method for finding an item in the store. # When an update comes through on the websocket, we need to figure out which item in the array it is # in order to match that item. # # Helpfully, on a data push (or on an HTTP hit to /api/v1/schema), we have a "keys" array. These are the # fields we can use to uniquely identify an item. Sometimes there is more than one, so we iterate through all # provided keys. def find_by_keys(keys, table, matchData): for item in table: if all(item[k] == matchData[k] for k in keys): return item def order_leaves_quantity(o): if o['leavesQty'] is None: return True return o['leavesQty'] > 0 ================================================ FILE: Helpers/Helpers.py ================================================ import json from SlaveContainer import SlaveContainer import logging import logging.config import sys import os import numpy as np # import pandas as pd import sqlite3 as sql ############################################ ## -- Helper Functions ############################################ ROOT_DIR = os.path.abspath(os.curdir) def create_logger(): # create logger logger = logging.getLogger('cct') # check if logger already been created if logger.hasHandlers(): return logger # Create handlers c_handler = logging.StreamHandler(stream=sys.stdout) f_handler = logging.FileHandler(f'{ROOT_DIR}/logs/cct.log', mode='w') c_handler.setLevel(logging.DEBUG) f_handler.setLevel(logging.DEBUG) # Create formatters and add it to handlers c_format = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') f_format = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') c_handler.setFormatter(c_format) f_handler.setFormatter(f_format) # Add handlers to the logger logger.addHandler(c_handler) logger.addHandler(f_handler) logger.setLevel(logging.DEBUG) return logger def server_begin(): ############################################ ## -- Server Preparation ############################################ logger = create_logger() logger.info('The Crypto-Copy-Trader starts launch') with open(f'{ROOT_DIR}/config_files/config.json', 'r') as config_f: config = json.load(config_f) logger.info('Reading configuration file...') logger.info(f'{len(config["slaves"])} Slave accounts detected') file = open(f'{ROOT_DIR}/config_files/symbols.csv', "r") symbols = file.readlines() slave_container = SlaveContainer(config, symbols) return slave_container ================================================ FILE: Helpers/Order.py ================================================ class Order: def __init__(self, price, amount, quantityPart, order_id, symbol, side, order_type, exchange, stop=0): self.price = price self.amount = amount self.quantityPart = quantityPart self.id = order_id self.symbol = symbol self.side = side self.type = order_type self.exchange = exchange self.stop = stop def __str__(self): return f"Order: id: {self.id}" \ f"price: {self.price}," \ f" symbol: {self.symbol}," \ f" amount: {self.amount}," \ f" part: {self.quantityPart}," \ f" side: {self.side}," \ f" type: {self.type}," def __repr__(self): return self.__str__() ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Crypto-Copy-Trader Copy trading tool for cryptocurrencies - written in Python & Flask
Makes you copy high-performing masters without doing any effort Disclaimer : this is a hobby project not intended for commercial use, and is provided as-is without warranty or liability. capital at risk. # Intro bot used to make a mass buying or selling of identical bots to do basic copy trading. #### Supported Exchanges - Binance Spot - Bitmex - Bitmex Testnet # Installation and Launch 1. Downland and install requirements ``` git clone https://github.com/MohammedRashad/Crypto-Copy-Trader.git cd Crypto-Copy-Trader pip install -r requirements.txt cp ./config_files/config-sample.json ./config_files/config.json ``` 2. Configure `config.json` - Open `./config_files/config.json` in text editor and paste your api keys and secrets to master and slaves - Possible values for a variable `exchange_name` you can find in folder ExchangeInterfaces. 3. Run `python api.py` 4. Open GUI - go to http://127.0.0.1:5000/ or http://0.0.0.0:5000/ - click `Run` button - see log in terminal or in file `logs/cct.log` - when will you see message `Launch complete` you can place the orders # Installation with Docker 1. Build Docker container ``` docker build -t crypto-copy-trader . ``` 2. Run first Docker container ``` docker run --publish 8000:5000 --detach --name crypto-copy-trader crypto-copy-trader ``` # Features - Database SQLite - Slave-Master Configuration - Copy active orders on launch - WebUI - Flask API - All orders Supported - Adding slaves in realtime - Ratio for not similar accounts - Built with bootstrap # Known Bugs - Add and delete slaves buttons not working with new config file. So need to fill `config.json` manually - database is not related with `config.json` - Bitmex working only with `XBTUSD` and `ETHUSD` pairs now - You may have an issue "ModuleNotFoundError: No module named 'jsonschema.compat'" (python 3.8.x) Please refer this post: https://stackoverflow.com/questions/69426664/modulenotfounderror-no-module-named-jsonschema-compat *solution*: pip install -Iv jsonschema==3.2.0 Please [open an issue](https://github.com/MohammedRashad/Crypto-Copy-Trader/issues/new) to help us fix any bugs or request features if needed. # Contributors Thanks to everyone trying to help, special thanks for [NickPius ](https://github.com/mokolotron) for multiple patches. Contact me if you want to join as a contributor. # License Apache 2.0 License ================================================ FILE: SlaveContainer.py ================================================ import asyncio from ExchangeInterfaces.BinanceExchange import BinanceExchange from ExchangeInterfaces.BitmexExchange import BitmexExchange from ExchangeInterfaces.BitmexTest import BitmexTest from ExchangeInterfaces.Exchange import Exchange import logging import Actions.Actions as Actions def factory_method_create_exchange(single_config, pairs) -> Exchange: exchange_name = single_config['exchange_name'] necessary_class = globals()[exchange_name] return necessary_class(single_config['key'], single_config['secret'], pairs, single_config['name']) class SlaveContainer: def __init__(self, config, pairs): self.logger = logging.getLogger('cct') self.logger.info(f"Connecting to the master: {config['master']['name']}...") slaves = [] try: self.master = factory_method_create_exchange(config['master'], pairs) self.logger.info("Connecting to the slaves. Its can take time...") for slave_config in config['slaves']: slave = factory_method_create_exchange(slave_config, pairs) if self.master.isMargin == slave.isMargin: slaves.append(slave) else: slave.stop() del slave except: self.logger.exception("Error initialing exchanges") self.slaves = slaves def start(self): self.logger.info("Open masters websocket... ") self.master.start(self.on_event_handler) self.logger.info('Launch complete. Now I can copy orders!') def stop(self): self.master.stop() for slave in self.slaves: slave.stop() def on_event_handler(self, event): # callback for event new order self.logger.debug(f'Event came: {event}') try: p_event = self.master.process_event(event) except: self.logger.exception('Error in master.process_event()') if p_event is None: # ignore this event return self.logger.info('\n') self.logger.info(f'New action came: {p_event}') if isinstance(p_event, Actions.ActionCancel): for slave in self.slaves: asyncio.run(slave.on_cancel_handler(p_event)) elif isinstance(p_event, Actions.ActionNewOrder): for slave in self.slaves: asyncio.run(slave.on_order_handler(p_event)) elif isinstance(p_event, Actions.ActionClosePosition): for slave in self.slaves: asyncio.run(slave.close_position(p_event)) # store order_id of master order to relate it with slave order if isinstance(p_event, Actions.ActionNewOrder): for slave in self.slaves: slave.ids.append(p_event.order.id) # delete already not existed order ids to avoid memory leak elif isinstance(p_event, (Actions.ActionClosePosition, Actions.ActionCancel)): ord_id = p_event.order_id for slave in self.slaves: if slave.is_program_order(ord_id): slave.delete_id(ord_id) def first_copy(self, orders): # copy open orders from master account to slaves for slave in self.slaves: for o in orders: asyncio.run(slave.async_create_order(o)) slave.balance_updated = False self.master.balance_updated = False ================================================ FILE: api.py ================================================ from flask import Flask, render_template, request, redirect from threading import Thread import sqlite3 as sql import csv from Helpers.Helpers import server_begin from SlaveContainer import SlaveContainer import logging app = Flask(__name__) stop_run = False test_false = True socket_usage = False def socket_function(container: SlaveContainer): container.start() # first_copy container.first_copy(container.master.get_open_orders()) # set variable for stop socket set_stop_run.container = container global socket_usage socket_usage = True def manual_run(): container = server_begin() t1 = Thread(target=socket_function, args=(container,)) t1.start() return "Processing" @app.route("/stop", methods=['GET']) def set_stop_run(): logger = logging.getLogger('cct') global stop_run if not stop_run: logger.warning('You cannot stop without starting. Think about it :)') return redirect("/") stop_run = False set_stop_run.container.stop() logger.info('WebSocket closed') return redirect("/", code=302) @app.route("/run", methods=['GET']) def run_process(): global stop_run if stop_run: logger = logging.getLogger('cct') logger.warning('The Program already has been running') return redirect("/") stop_run = True manual_run() return redirect("/", code=302) @app.route('/master', methods=['POST']) def master_form(): print(request.form['comment_content']) print(request.form['comment_content2']) print(request.form['comment_content3']) with sql.connect("database.db") as con: cur = con.cursor() cur.execute("INSERT INTO keys (name,key,secret,type) VALUES (?,?,?,?)", ( request.form['comment_content3'], request.form['comment_content'], request.form['comment_content2'], "master")) con.commit() print("Record successfully added") con.close() return redirect("/", code=302) @app.route('/delete_master') def delete_master(): with sql.connect("database.db") as con: cur = con.cursor() cur.execute("delete from keys where type='master'") con.commit() print("Record successfully deleted") con.close() return redirect("/", code=302) @app.route('/delete_slave') def delete_slave(): with sql.connect("database.db") as con: cur = con.cursor() cur.execute("delete from keys where type='slave'") con.commit() print("Record successfully deleted") con.close() return redirect("/", code=302) @app.route('/slave', methods=['POST']) def slave_form(): print(request.form['comment_content']) print(request.form['comment_content2']) print(request.form['comment_content3']) with sql.connect("database.db") as con: cur = con.cursor() cur.execute("INSERT INTO keys (name,key,secret,type) VALUES (?,?,?,?)", ( request.form['comment_content3'], request.form['comment_content'], request.form['comment_content2'], "slave")) con.commit() print("Record successfully added") con.close() return redirect("/", code=302) @app.route('/') def homepage(): global test_false if test_false == True: test_false = False final = bool(test_false) ^ bool(stop_run) con = sql.connect("database.db") con.row_factory = sql.Row cur = con.cursor() cur.execute("select * from keys where type='slave'") rows = cur.fetchall() cur = con.cursor() cur.execute("select * from keys where type='master'") rows2 = cur.fetchall() slave_keys = [] slave_sec = [] master_key = [] master_sec = [] for row in rows: slave_keys.append(row["key"]) slave_sec.append(row["secret"]) for row in rows2: master_key.append(row["key"]) master_sec.append(row["secret"]) with open('config_files/config.csv', mode='w', newline='') as file: writer = csv.writer(file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) writer.writerow(['Master API Key'] + master_key + [""]) writer.writerow(['Master API Keys'] + master_sec + [""]) writer.writerow(['Slave API Keys'] + slave_keys + [""]) writer.writerow(['Slave API Secrets'] + slave_sec + [""]) final_str = "No" if False else "Yes" return render_template("home.html", isRunning="Is App Running ? : " + final_str, rows=rows, rows2=rows2) if __name__ == "__main__": app.run(host='0.0.0.0', debug=True) ================================================ FILE: changelog.md ================================================ ================================================ FILE: config_files/config-sample.json ================================================ { "master": { "name": "Master" , "key":"" , "secret" :"", "exchange_name": "BitmexExchange" }, "slaves": [{ "name": "Slave1", "key": "", "secret": "", "exchange_name": "BitmexExchange" }, { "name": "Slave2", "key": "", "secret": "", "exchange_name": "BinanceExchange" } ], "settings": { } } ================================================ FILE: config_files/config.csv ================================================ Master API Key,NBVX3hwfLkadvNUPJwIxUPkOWPlBfvMJivnrlnRhZZzvNveKI0FBKVLHlnj7KU5p, Master API Keys,R0FXuxetL9NmLqRCkEleKcHm99Nbcaxt4PaSXv0wfxHFjFgD0SCA0rXEIqAbVB3S, Slave API Keys,QZcFeRrlqMBeu4qBWunVDIaAGZ7oq4TDZBexILfSS2vzEZGwcUQlE5cD1u5468QY,bOnKUP8bqg6PvT76o0TbIZf0wOtEVVe29GxmQT6WRvQ64fMYJUgtjYrCHhsWSlCG, Slave API Secrets,61kVTcJZ0NgXtHxneprQocqb9OUqzwXLTJ1FZvThHzu9ka9DBDtexc8CSMx2xA4k,sPInEZ7uRlSF0eTm1h0XmYL2JW1hkETc36ziJYM9jvYl8G8NhQwpAYQ1WI9MgWQd, ================================================ FILE: config_files/symbols.csv ================================================ ETHUSDT BTCUSDT ETHBTC LTCBTC ================================================ FILE: config_files/symbols2.csv ================================================ THETAUSDT ENJUSDT MITHUSDT MATICBTC MATICUSDT ATOMBTC ATOMUSDT PHBBTC TFUELBTC TFUELUSDT ONEBTC ONEUSDT FTMBTC FTMUSDT BTCBBTC ALGOBTC ALGOUSDT GTOUSDT ERDBTC ERDUSDT DOGEBTC DOGEUSDT DUSKBTC DUSKUSDT ANKRBTC ANKRUSDT WINBTC WINUSDT COSBTC COSUSDT NPXSUSDT COCOSBTC COCOSUSDT MTLUSDT TOMOBTC TOMOUSDT PERLBTC PERLUSDT KEYUSDT STORMUSDT DOCKUSDT WANUSDT FUNUSDT CVCUSDT BTTTRX WINTRX CHZBTC CHZUSDT BANDBTC BANDUSDT BTCBUSD BUSDUSDT BEAMBTC BEAMUSDT XTZBTC XTZUSDT RENUSDT RVNUSDT HCUSDT HBARBTC HBARUSDT NKNBTC NKNUSDT XRPBUSD BCHABCBUSD LTCBUSD LINKBUSD ETCBUSD STXBTC STXUSDT KAVABTC KAVAUSDT BUSDNGN BTCNGN ARPABTC ARPAUSDT TRXBUSD EOSBUSD IOTXUSDT RLCUSDT MCOUSDT XLMBUSD ADABUSD CTXCBTC CTXCUSDT BCHBTC BCHUSDT BCHBUSD BTCRUB XRPRUB TROYBTC TROYUSDT BUSDRUB QTUMBUSD VETBUSD VITEBTC VITEUSDT NEOBTC BCCBTC GASBTC HSRBTC MCOBTC WTCBTC LRCBTC QTUMBTC YOYOBTC OMGBTC ZRXBTC ================================================ FILE: config_files/symbols3.csv ================================================ STORMBTC QTUMUSDT XEMBTC WANBTC WPRBTC QLCBTC SYSBTC GRSBTC ADAUSDT CLOAKBTC GNTBTC LOOMBTC XRPUSDT BCNBTC REPBTC ZENBTC SKYBTC EOSUSDT CVCBTC THETABTC IOTAUSDT XLMUSDT IOTXBTC QKCBTC AGIBTC NXSBTC DATABTC TRXUSDT ETCUSDT ICXUSDT SCBTC NPXSBTC VENUSDT KEYBTC NASBTC MFTBTC DENTBTC ARDRBTC NULSUSDT HOTBTC VETBTC DOCKBTC POLYBTC PHXBTC HCBTC GOBTC RVNBTC DCRBTC MITHBTC BCHABCBTC BCHSVBTC BCHABCUSDT BCHSVUSDT RENBTC TRXXRP XZCXRP LINKUSDT WAVESUSDT BTTBTC ONGBTC ONGUSDT ZILUSDT ZRXUSDT FETBTC XMRUSDT ZECUSDT CELRBTC CELRUSDT DASHUSDT NANOUSDT OMGUSDT STRATBTC SNGLSBTC BQXBTC KNCBTC FUNBTC SNMBTC IOTABTC LINKBTC XVGBTC SALTBTC MDABTC MTLBTC SUBBTC EOSBTC SNTBTC ETCBTC MTHBTC ENGBTC DNTBTC ZECBTC BNTBTC ASTBTC DASHBTC OAXBTC ICNBTC ================================================ FILE: config_files/symbols4.csv ================================================ RCNBTC NULSBTC RDNBTC XMRBTC DLTBTC AMBBTC BCCUSDT BATBTC BCPTBTC ARNBTC GVTBTC CDTBTC GXSBTC NEOUSDT POEBTC QSPBTC BTSBTC XZCBTC LSKBTC TNTBTC FUELBTC MANABTC BCDBTC DGDBTC ADXBTC ADABTC PPTBTC CMTBTC XLMBTC CNDBTC LENDBTC WABIBTC LTCUSDT TNBBTC WAVESBTC GTOBTC ICXBTC OSTBTC ELFBTC AIONBTC NEBLBTC BRDBTC EDOBTC WINGSBTC NAVBTC LUNBTC TRIGBTC APPCBTC VIBEBTC RLCBTC INSBTC PIVXBTC IOSTBTC CHATBTC STEEMBTC NANOBTC VIABTC BLZBTC AEBTC RPXBTC NCASHBTC POABTC ZILBTC ONTBTC BTGBTC EVXBTC REQBTC VIBBTC TRXBTC POWRBTC ARKBTC XRPBTC MODBTC ENJBTC STORJBTC VENBTC KMDBTC ================================================ FILE: logs/cct.log ================================================ 2020-09-21 20:55:40,426 - INFO - The Crypto-Copy-Trader starts launch 2020-09-21 20:55:40,427 - INFO - Reading configuration file... 2020-09-21 20:55:40,428 - INFO - 2 Slave accounts detected 2020-09-21 20:55:40,428 - DEBUG - Can't translate word ETHBTC in Bitmex 2020-09-21 20:55:40,432 - DEBUG - Can't translate word LTCBTC in Bitmex 2020-09-21 20:55:45,105 - DEBUG - Can't translate word ETHBTC in Bitmex 2020-09-21 20:55:45,105 - DEBUG - Can't translate word LTCBTC in Bitmex 2020-09-21 20:55:52,045 - DEBUG - {'table': 'order', 'action': 'partial', 'keys': ['orderID'], 'types': {'orderID': 'guid', 'clOrdID': 'string', 'clOrdLinkID': 'symbol', 'account': 'long', 'symbol': 'symbol', 'side': 'symbol', 'simpleOrderQty': 'float', 'orderQty': 'long', 'price': 'float', 'displayQty': 'long', 'stopPx': 'float', 'pegOffsetValue': 'float', 'pegPriceType': 'symbol', 'currency': 'symbol', 'settlCurrency': 'symbol', 'ordType': 'symbol', 'timeInForce': 'symbol', 'execInst': 'symbol', 'contingencyType': 'symbol', 'exDestination': 'symbol', 'ordStatus': 'symbol', 'triggered': 'symbol', 'workingIndicator': 'boolean', 'ordRejReason': 'symbol', 'simpleLeavesQty': 'float', 'leavesQty': 'long', 'simpleCumQty': 'float', 'cumQty': 'long', 'avgPx': 'float', 'multiLegReportingType': 'symbol', 'text': 'string', 'transactTime': 'timestamp', 'timestamp': 'timestamp'}, 'foreignKeys': {'symbol': 'instrument', 'side': 'side', 'ordStatus': 'ordStatus'}, 'attributes': {'orderID': 'grouped', 'account': 'grouped', 'ordStatus': 'grouped', 'workingIndicator': 'grouped'}, 'filter': {'account': 308752, 'symbol': 'XBTUSD'}, 'data': [{'orderID': '8705e1a0-75af-1dfa-14d4-a54070eec4e0', 'clOrdID': '', 'clOrdLinkID': '', 'account': 308752, 'symbol': 'XBTUSD', 'side': 'Buy', 'simpleOrderQty': None, 'orderQty': 40, 'price': 9500.5, 'displayQty': None, 'stopPx': None, 'pegOffsetValue': None, 'pegPriceType': '', 'currency': 'USD', 'settlCurrency': 'XBt', 'ordType': 'Limit', 'timeInForce': 'GoodTillCancel', 'execInst': '', 'contingencyType': '', 'exDestination': 'XBME', 'ordStatus': 'New', 'triggered': '', 'workingIndicator': True, 'ordRejReason': '', 'simpleLeavesQty': None, 'leavesQty': 40, 'simpleCumQty': None, 'cumQty': 0, 'avgPx': None, 'multiLegReportingType': 'SingleSecurity', 'text': 'Submission from testnet.bitmex.com', 'transactTime': '2020-09-21T17:55:48.385Z', 'timestamp': '2020-09-21T17:55:48.385Z'}]} 2020-09-21 20:55:53,137 - DEBUG - {'table': 'order', 'action': 'partial', 'keys': ['orderID'], 'types': {'orderID': 'guid', 'clOrdID': 'string', 'clOrdLinkID': 'symbol', 'account': 'long', 'symbol': 'symbol', 'side': 'symbol', 'simpleOrderQty': 'float', 'orderQty': 'long', 'price': 'float', 'displayQty': 'long', 'stopPx': 'float', 'pegOffsetValue': 'float', 'pegPriceType': 'symbol', 'currency': 'symbol', 'settlCurrency': 'symbol', 'ordType': 'symbol', 'timeInForce': 'symbol', 'execInst': 'symbol', 'contingencyType': 'symbol', 'exDestination': 'symbol', 'ordStatus': 'symbol', 'triggered': 'symbol', 'workingIndicator': 'boolean', 'ordRejReason': 'symbol', 'simpleLeavesQty': 'float', 'leavesQty': 'long', 'simpleCumQty': 'float', 'cumQty': 'long', 'avgPx': 'float', 'multiLegReportingType': 'symbol', 'text': 'string', 'transactTime': 'timestamp', 'timestamp': 'timestamp'}, 'foreignKeys': {'symbol': 'instrument', 'side': 'side', 'ordStatus': 'ordStatus'}, 'attributes': {'orderID': 'grouped', 'account': 'grouped', 'ordStatus': 'grouped', 'workingIndicator': 'grouped'}, 'filter': {'account': 308752, 'symbol': 'ETHUSD'}, 'data': []} 2020-09-21 20:55:55,274 - INFO - Launch complete. Now I can copy orders 2020-09-21 20:55:55,280 - INFO - Slave Bitmex, balance: 900419; Create Order: amount 39.28880294330567, price: 9500.5 2020-09-21 20:55:55,417 - INFO - Create order request send. Response: ({'orderID': '734bece5-8e45-f12d-19a5-8ce9ea8056a4', 'clOrdID': '8705e1a0-75af-1dfa-14d4-a54070eec4e0', 'clOrdLinkID': '', 'account': 311240, 'symbol': 'XBTUSD', 'side': 'Buy', 'simpleOrderQty': None, 'orderQty': 39, 'price': 9500.5, 'displayQty': None, 'stopPx': None, 'pegOffsetValue': None, 'pegPriceType': '', 'currency': 'USD', 'settlCurrency': 'XBt', 'ordType': 'Limit', 'timeInForce': 'GoodTillCancel', 'execInst': '', 'contingencyType': '', 'exDestination': 'XBME', 'ordStatus': 'New', 'triggered': '', 'workingIndicator': True, 'ordRejReason': '', 'simpleLeavesQty': None, 'leavesQty': 39, 'simpleCumQty': None, 'cumQty': 0, 'avgPx': None, 'multiLegReportingType': 'SingleSecurity', 'text': 'Submitted via API.', 'transactTime': datetime.datetime(2020, 9, 21, 17, 55, 55, 378000, tzinfo=tzutc()), 'timestamp': datetime.datetime(2020, 9, 21, 17, 55, 55, 378000, tzinfo=tzutc())}, ) ================================================ FILE: requirements.txt ================================================ flask==1.0.2 cython python_binance==0.7.4 deepdiff==4.0.9 numpy>=1.13.3 binance==0.3 bitmex bitmex_ws==0.4.0 ================================================ FILE: static/styles/base.css ================================================ body { background: rgb(255, 255, 255); color: rgb(0, 0, 0); } .navbar { /* border-bottom: #000 3px solid; */ opacity: 1; } .container { font-size: 1.3rem; } .btn-primary-outline { background-color: #ffffff; border-color: rgb(17, 21, 252); } .uploader-input { background: #fff; color: #696969; font-size: 1rem; } .form-check-label { font-size: 1rem; color: #a9a9a9; } /* #home-section { background: url("../img/home.jpg") no-repeat; min-height: 700px; background-size: cover; background-attachment: fixed; } #home-section .dark-overlay { background-color: rgba(0, 0, 0, 0.7); position: absolute; top: 0; left: 0; width: 100%; min-height: 700px; } #home-section .home-inner { padding-top: 150px; } #home-section .card-form { opacity: 0.8; } #home-section .fas { color: #008ed6; background: #fff; padding: 4px; border-radius: 5px; font-size: 30px; } #explore-section .fas, #share-section .fas { color: #fff; background: #333; padding: 4px; border-radius: 5px; font-size: 30px; } #create-section .fas { color: #008ed6; background: #fff; padding: 4px; border-radius: 5px; font-size: 30px; } */ ================================================ FILE: templates/home.html ================================================ {% extends 'layout.html' %} {% block body %}


Binance Cryptocurrency CopyTrader

Easily Scale your Cryptocurrency trading workflow with automatic copying and trading




{{isRunning}}

Run Stop
Add/Replace Master Account

Master API Key :   

Master API Secret :

Master API Name :


Add New Slave

Slave API Key :   

Slave API Secret :

Slave API Name :


Current Master
{% for row in rows2 %} {% endfor %}
Name Key Secret
{{row["name"]}} {{row["key"]}} {{ row["secret"]}}
Delete Master
Slaves List
{% for row in rows %} {% endfor %}
Name Key Secret
{{row["name"]}} {{row["key"]}} {{ row["secret"]}}
Delete Slaves
{% endblock %} ================================================ FILE: templates/includes/_formhelpers.html ================================================ {% macro render_field(field) %} {{ field.label }} {{ field(**kwargs)|safe }} {% if field.errors %} {% for error in field.errors %} {{ error }} {% endfor %} {% endif %} {% endmacro %} ================================================ FILE: templates/includes/_messages.html ================================================ {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %} {% if error %}
{{error}}
{% endif %} {% if msg %}
{{msg}}
{% endif %} ================================================ FILE: templates/includes/_navbar.html ================================================ ================================================ FILE: templates/layout.html ================================================ Binance Cryptocurrency CopyTrader {% include 'includes/_navbar.html' %}
{% include 'includes/_messages.html' %} {% block body %}{% endblock%}
{% block end_body %}{% endblock %}