├── .gitignore ├── LICENSE ├── README.md ├── arbitrage ├── __init__.py ├── arbitrage.ini ├── arbitrator.py ├── b2u.py ├── basebit.py ├── bitfinex.py ├── bitinvest.py ├── bitstamp.py ├── client.py ├── hitbtc.py ├── itbit.py ├── mb.py ├── order_book_processor.py └── util.py ├── bitcoind ├── Dockerfile ├── bin │ ├── blocknotify │ ├── btc_init │ ├── btc_oneshot │ └── walletnotify └── docker-entrypoint.sh ├── bitex.ini ├── config ├── bootstrap.py └── demo.ini ├── docker-compose.yml ├── libraries ├── json_encoder.py ├── message.py ├── message_builder.py ├── project_options.py ├── signals.py ├── utils.py └── zmq_client.py ├── mailer ├── __init__.py ├── main.py ├── mandrill.py ├── templates │ ├── customer-verification-submit-en.txt │ ├── order-execution-en.txt │ ├── password-reset-en.txt │ ├── welcome-en.txt │ ├── withdraw-cancelled-en.txt │ ├── withdraw-confirmation-bank-transfer-en.txt │ ├── withdraw-confirmation-bitcoin-en.txt │ └── your-account-has-been-verified-en.txt └── util.py ├── match ├── Dockerfile ├── __init__.py ├── decorators.py ├── errors.py ├── execution.py ├── main.py ├── market_data_publisher.py ├── models.py ├── session.py ├── session_manager.py ├── tests │ ├── __init__.py │ └── test_model.py ├── trade_application.py └── views.py ├── nginx ├── Dockerfile └── nginx.conf ├── receiver ├── Dockerfile ├── api_receive.ini ├── api_receive_application.py ├── authproxy.py ├── block_notify_handler.py ├── create_receive_handler.py ├── main.py ├── models.py └── wallet_notify_handler.py ├── requirements.txt ├── ssl ├── Dockerfile └── generate-server-certs └── ws_gateway ├── Dockerfile ├── __init__.py ├── deposit_hander.py ├── deposit_receipt_webhook_handler.py ├── instrument_helper.py ├── main.py ├── market_data_helper.py ├── models.py ├── process_deposit_handler.py ├── rest_api_handler.py ├── tests ├── __init__.py └── test_signals.py ├── util.py └── verification_webhook_handler.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitex Crypto Exchange 2 | 3 | ## Joining the blockchain 4 | 5 | 1. Install [Docker](https://www.docker.com/) 6 | 2. Install [Docker-Compose](https://docs.docker.com/compose/) 7 | 3. `docker-compose up` 8 | 4. tun run as deamon 9 | 5. `docker-compose up -d` 10 | 11 | Setup will spin of next services: 12 | - http://localhost:8080/ -- Demo Exchange 13 | - http://localhost:61208/ -- Exchange Status and Monitoring 14 | 15 | Stop all containers: `docker ps -aq | xargs docker stop` 16 | -------------------------------------------------------------------------------- /arbitrage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitex-coin/backend/7296fb74988e9c7d64db8fcdd0eeb69f15b041ec/arbitrage/__init__.py -------------------------------------------------------------------------------- /arbitrage/arbitrage.ini: -------------------------------------------------------------------------------- 1 | [bitstamp1] 2 | websocket_url= ws://127.0.0.1:8445/ 3 | username = bzero 4 | password = senha123 5 | buy_fee = 0 6 | sell_fee = 0 7 | broker_id=5 8 | dest_market=BTCUSD 9 | 10 | [bitstamp] 11 | websocket_url= ws://127.0.0.1:8445/ 12 | username = bzero 13 | password = senha123 14 | buy_fee = -2.00 15 | sell_fee = 2.00 16 | broker_id=5 17 | dest_market=BTCUSD 18 | 19 | 20 | [b2u] 21 | websocket_url= ws://127.0.0.1:8445/ 22 | broker_id=5 23 | username = bzero 24 | password = senha123 25 | buy_fee = 0 26 | sell_fee = 0 27 | broker_id=5 28 | dest_market=BTCUSD 29 | 30 | 31 | [basebit] 32 | websocket_url= ws://127.0.0.1:8445/ 33 | username = bzero 34 | password = senha123 35 | buy_fee = 0 36 | sell_fee = 0 37 | broker_id=5 38 | dest_market=BTCUSD 39 | 40 | 41 | [bitfinex] 42 | websocket_url= ws://127.0.0.1:8445/ 43 | username = bzero 44 | password = senha123 45 | buy_fee = 0 46 | sell_fee = 0 47 | broker_id=5 48 | dest_market=BTCUSD 49 | 50 | 51 | [bitinvest] 52 | websocket_url= ws://127.0.0.1:8445/ 53 | username = bzero 54 | password = senha123 55 | buy_fee = 0 56 | sell_fee = 0 57 | subscription_api_key = fc848c1b8ead44bc99db3a80f7dfb882 58 | broker_id=5 59 | dest_market=BTCUSD 60 | 61 | 62 | [hitbtc] 63 | websocket_url= ws://127.0.0.1:8445/ 64 | username = bzero 65 | password = senha123 66 | buy_fee = 0 67 | sell_fee = 0 68 | broker_id=5 69 | dest_market=BTCUSD 70 | 71 | [itbit1] 72 | websocket_url= ws://127.0.0.1:8445/ 73 | username = bzero 74 | password = senha123 75 | buy_fee = 0 76 | sell_fee = 0 77 | broker_id=5 78 | dest_market=BTCUSD 79 | 80 | [mb] 81 | websocket_url= ws://127.0.0.1:8445/ 82 | username = bzero 83 | password = senha123 84 | buy_fee = 0 85 | sell_fee = 0 86 | broker_id=5 87 | dest_market=BTCUSD 88 | 89 | -------------------------------------------------------------------------------- /arbitrage/arbitrator.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | import time 5 | 6 | ROOT_PATH = os.path.abspath( os.path.join(os.path.dirname(__file__), "../../")) 7 | sys.path.insert( 0, os.path.join(ROOT_PATH, 'libs')) 8 | 9 | from order_book_processor import OrderBookProcessor 10 | from util import get_funded_entries, aggregate_orders 11 | from pyblinktrade.signals import Signal 12 | from client import BitExThreadedClient 13 | import json 14 | import datetime 15 | import time 16 | 17 | class BlinkTradeArbitrator(object): 18 | def __init__(self, blinktrade_broker_id, blinktrade_username, blinktrade_password, blinktrade_ws_url='wss://api.blinktrade.com/trade/', symbol='BTCUSD'): 19 | self.fiat_currency = symbol[3:] 20 | self.crypto_currency = symbol[:3] 21 | self.fiat_balance = 0 22 | self.crypto_balance = 0 23 | self.latency = 0 24 | self.blinktrade_ws_url = blinktrade_ws_url 25 | self.blinktrade_broker_id = blinktrade_broker_id 26 | self.blinktrade_username = blinktrade_username 27 | self.blinktrade_password = blinktrade_password 28 | self.blinktrade_broker = None 29 | self.blinktrade_profile = None 30 | 31 | self.order_book_bid_processor = OrderBookProcessor('1', symbol) 32 | self.order_book_ask_processor = OrderBookProcessor('2', symbol) 33 | self.order_book_bid_processor.send_new_order_signal.connect(self.on_send_buy_new_order) 34 | self.order_book_ask_processor.send_new_order_signal.connect(self.on_send_sell_new_order) 35 | self.order_book_bid_processor.cancel_order_signal.connect(self.on_send_cancel_order) 36 | self.order_book_ask_processor.cancel_order_signal.connect(self.on_send_cancel_order) 37 | 38 | #Signals 39 | self.signal_connected = Signal() 40 | self.signal_disconnected = Signal() 41 | self.signal_logged = Signal() 42 | self.signal_order = Signal() 43 | 44 | 45 | self.create_websocket() 46 | 47 | def create_websocket(self): 48 | self.ws = BitExThreadedClient( self.blinktrade_ws_url ) 49 | self.ws.signal_connection_open.connect(self.on_ws_open) 50 | self.ws.signal_connection_closed.connect(self.on_ws_closed) 51 | self.ws.signal_heartbeat.connect(self.on_blinktrade_heartbeat) 52 | self.ws.signal_logged.connect(self.on_blinktrade_connected) 53 | self.ws.signal_balance.connect(self.on_blinktrade_balance ) 54 | self.ws.signal_execution_report.connect(self.on_blinktrade_execution_report) 55 | self.ws.signal_send.connect(self.on_blinktrade_send) 56 | self.ws.signal_recv.connect(self.on_blinktrade_recv) 57 | 58 | def is_connected(self): 59 | return self.ws.is_connected 60 | 61 | def is_logged(self): 62 | return self.ws.is_logged 63 | 64 | def on_ws_open(self, sender, msg): 65 | self.signal_connected(self) 66 | time.sleep(2) 67 | self.ws.login(self.blinktrade_broker_id, self.blinktrade_username, self.blinktrade_password ) 68 | 69 | def on_ws_closed(self, sender, code_reason): 70 | self.signal_disconnected(self, code_reason) 71 | 72 | def reconnect(self): 73 | del self.ws 74 | self.create_websocket() 75 | self.connect() 76 | 77 | def connect(self): 78 | from ws4py.exc import HandshakeError 79 | try: 80 | self.ws.connect() 81 | except HandshakeError, e: 82 | print datetime.datetime.now(), 'ERROR', 'connection error: ', e 83 | raise 84 | 85 | def close(self): 86 | self.ws.close() 87 | 88 | def on_blinktrade_send(self, sender, msg): 89 | print datetime.datetime.now(), 'SEND', msg 90 | 91 | def on_blinktrade_recv(self, sender, msg): 92 | print datetime.datetime.now(), 'RECV', msg 93 | 94 | def on_blinktrade_execution_report(self, sender, msg): 95 | if msg['ExecType'] == '0' or msg['ExecType'] == '4': # cancel 96 | return 97 | self.signal_order(self, { 98 | 'MsgType' : 'D', 99 | 'Symbol' : msg['Symbol'], 100 | 'OrderQty' : msg['LastShares'], 101 | 'Price' : msg['Price'], 102 | 'OrdType' : '2', # Limited 103 | 'Side' : '1' if msg['Side'] == '2' else '2' 104 | } ) 105 | 106 | def on_blinktrade_balance(self, sender, msg): 107 | if str(self.blinktrade_broker['BrokerID']) in msg: 108 | if self.fiat_currency in msg[str(self.blinktrade_broker['BrokerID'])]: 109 | self.fiat_balance = msg[str(self.blinktrade_broker['BrokerID'])][self.fiat_currency] 110 | if self.crypto_currency in msg[str(self.blinktrade_broker['BrokerID'])]: 111 | self.crypto_balance = msg[str(self.blinktrade_broker['BrokerID'])][self.crypto_currency] 112 | 113 | def on_blinktrade_connected(self, sender, msg): 114 | self.signal_logged(sender, msg) 115 | print 'connected to blinktrade' 116 | self.blinktrade_broker = msg['Broker'] 117 | self.blinktrade_profile = msg['Profile'] 118 | self.ws.send(json.dumps({ 'MsgType':'F'})) # Cancel all open orders for this user 119 | self.ws.requestBalances() 120 | 121 | 122 | def on_send_buy_new_order(self,sender, msg): 123 | self.ws.sendMsg(msg) 124 | 125 | def on_send_sell_new_order(self,sender, msg): 126 | self.ws.sendMsg(msg) 127 | 128 | def on_send_cancel_order(self,sender, msg): 129 | self.ws.sendMsg(msg) 130 | 131 | def process_bid_list(self, bid_list ): 132 | bid_list = get_funded_entries(bid_list, self.fiat_balance, True) 133 | bid_list = aggregate_orders(bid_list) 134 | self.order_book_bid_processor.process_order_list(bid_list) 135 | 136 | def process_ask_list(self, ask_list): 137 | ask_list = get_funded_entries(ask_list, self.crypto_balance, False) 138 | ask_list = aggregate_orders(ask_list) 139 | self.order_book_ask_processor.process_order_list(ask_list) 140 | 141 | def on_blinktrade_heartbeat(self, msg): 142 | received_timestamp = int(time.time()*1000) 143 | sent_timestamp = msg['TestReqID'] 144 | self.latency = received_timestamp - sent_timestamp 145 | 146 | 147 | def send_testRequest(self): 148 | self.ws.testRequest( int(time.time()*1000) ) 149 | 150 | def run(self): 151 | self.ws.run_forever() 152 | 153 | def cancel_all_orders(self): 154 | self.ws.send(json.dumps({'MsgType':'F'})) # Cancel all open orders for this user 155 | 156 | -------------------------------------------------------------------------------- /arbitrage/b2u.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import urllib2 4 | from time import sleep 5 | import json 6 | 7 | from arbitrator import BlinkTradeArbitrator 8 | 9 | import datetime 10 | import hmac 11 | import hashlib 12 | import ConfigParser 13 | from ws4py.exc import HandshakeError 14 | 15 | 16 | B2U_API_KEY = 'XXXX' 17 | B2U_API_SECRET = 'YYYY' 18 | 19 | def send_order_to_b2u(sender, order): 20 | nonce = datetime.datetime.now().strftime('%s') 21 | message = str(nonce) + '.blinktrade.' + str(B2U_API_KEY) 22 | signature = hmac.new(B2U_API_SECRET, msg=message, digestmod=hashlib.sha256).hexdigest().upper() 23 | 24 | post_params = { 25 | 'key': B2U_API_KEY, 26 | 'signature': signature, 27 | 'nonce': nonce, 28 | 'amount': float(order['OrderQty']/1.e8), 29 | 'price': float(order['Price'] / 1.e8) 30 | } 31 | 32 | if msg['Side'] == '1': 33 | print datetime.datetime.now(), 'POST https://www.bitcointoyou.com/api/buy/', str(post_params) 34 | elif msg['Side'] == '2': 35 | print datetime.datetime.now(), 'POST https://www.bitcointoyou.com/api/sell/', str(post_params) 36 | 37 | 38 | def main(): 39 | candidates = ['arbitrage.ini', 'b2u.ini' ] 40 | if len(sys.argv) > 1: 41 | candidates.append(sys.argv[1]) 42 | 43 | 44 | config = ConfigParser.SafeConfigParser({ 45 | 'websocket_url': 'wss://127.0.0.1/trade/', 46 | 'username': '', 47 | 'password': '', 48 | 'broker_id': 5, 49 | 'buy_fee': 0, 50 | 'sell_fee': 0, 51 | 'api_key': 'KEY', 52 | 'api_secret': 'SECRET' 53 | }) 54 | config.read( candidates ) 55 | 56 | websocket_url = config.get('b2u', 'websocket_url') 57 | username = config.get('b2u', 'username') 58 | password = config.get('b2u', 'password') 59 | buy_fee = int(config.get('b2u', 'buy_fee')) 60 | sell_fee = int(config.get('b2u', 'sell_fee')) 61 | api_key = config.get('b2u', 'api_key') 62 | api_secret = config.get('b2u', 'api_secret') 63 | broker_id = config.getint('b2u', 'broker_id') 64 | dest_market = config.get('b2u', 'dest_market') 65 | 66 | print 'websocket_url:', websocket_url 67 | print 'username:', username 68 | print 'buy_fee:', buy_fee 69 | print 'sell_fee:', sell_fee 70 | 71 | arbitrator = BlinkTradeArbitrator( broker_id, username,password,websocket_url, dest_market) 72 | arbitrator.connect() 73 | 74 | #arbitrator.signal_order.connect(send_order_to_b2u) 75 | 76 | while True: 77 | try: 78 | sleep(10) 79 | if arbitrator.is_connected(): 80 | arbitrator.send_testRequest() 81 | else: 82 | try: 83 | arbitrator.reconnect() 84 | except HandshakeError,e: 85 | continue 86 | 87 | try: 88 | raw_data = urllib2.urlopen('https://www.bitcointoyou.com/API/orderbook.aspx').read() 89 | except Exception as e: 90 | print 'ERROR RETRIEVING ORDER BOOK: ' + str(e) 91 | continue 92 | 93 | 94 | bids_asks = [] 95 | try: 96 | bids_asks = json.loads(raw_data) 97 | except Exception : 98 | try: 99 | bids_asks = json.loads(raw_data.replace('][','],[')) # bug with b2u api 100 | except Exception : 101 | pass 102 | pass 103 | 104 | if bids_asks: 105 | ask_list = [ [ int(float(fiat)*1e8 * (1. + sell_fee) ), int(float(btc) * 1e8) ] for fiat,btc in bids_asks['asks'] ] 106 | bid_list = [ [ int(float(fiat)*1e8 * (1. - buy_fee) ), int(float(btc) * 1e8) ] for fiat,btc in bids_asks['bids'] ] 107 | 108 | number_of_asks_to_remove_due_a_weird_bug = 0 109 | for ask_price, ask_size in ask_list: 110 | if ask_price < bid_list[0][0]: 111 | number_of_asks_to_remove_due_a_weird_bug += 1 112 | else: 113 | break 114 | if number_of_asks_to_remove_due_a_weird_bug: 115 | print datetime.datetime.now(), 'Those sell orders are weird => ', [ 'BTC {:,.8f}'.format(s/1e8) + ' @ R$ {:,.2f}'.format(p/1e8) for p, s in ask_list[:number_of_asks_to_remove_due_a_weird_bug] ] 116 | 117 | ask_list = ask_list[number_of_asks_to_remove_due_a_weird_bug:] 118 | 119 | arbitrator.process_ask_list(ask_list) 120 | arbitrator.process_bid_list(bid_list) 121 | 122 | except urllib2.URLError as e: 123 | print datetime.datetime.now(), e 124 | 125 | except KeyboardInterrupt: 126 | arbitrator.cancel_all_orders() 127 | print 'wait....' 128 | sleep(5) 129 | arbitrator.close() 130 | break 131 | 132 | main() 133 | 134 | -------------------------------------------------------------------------------- /arbitrage/basebit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import urllib2 4 | from time import sleep 5 | import json 6 | 7 | from arbitrator import BlinkTradeArbitrator 8 | 9 | import datetime 10 | import hmac 11 | import hashlib 12 | import ConfigParser 13 | from ws4py.exc import HandshakeError 14 | 15 | 16 | BASEBIT_API_KEY = 'XXXX' 17 | BASEBIT_API_SECRET = 'YYYY' 18 | 19 | def send_order_to_basebit(sender, order): 20 | nonce = datetime.datetime.now().strftime('%s') 21 | message = 'sendorder' + str(BASEBIT_API_KEY) + str(nonce) 22 | signature = hmac.new(BASEBIT_API_SECRET, msg=message, digestmod=hashlib.sha256).hexdigest().upper() 23 | 24 | post_params = { 25 | 'key': BASEBIT_API_KEY, 26 | 'sign': signature, 27 | 'pair': order['Symbol'], 28 | 'quantity': float(order['OrderQty']/1.e8), 29 | 'price': float( order['Price'] / 1.e8) 30 | } 31 | 32 | if msg['Side'] == '1': 33 | post_params['type'] = 'buy' 34 | elif msg['Side'] == '2': 35 | post_params['type'] = 'sell' 36 | 37 | print datetime.datetime.now(), 'POST https://www.basebit.com.br/secure/tapi/' + message, str(post_params) 38 | 39 | def main(): 40 | candidates = ['arbitrage.ini', 'basebit.ini' ] 41 | if len(sys.argv) > 1: 42 | candidates.append(sys.argv[1]) 43 | 44 | 45 | config = ConfigParser.SafeConfigParser({ 46 | 'websocket_url': 'wss://127.0.0.1/trade/', 47 | 'username': '', 48 | 'password': '', 49 | 'buy_fee': 0, 50 | 'sell_fee': 0, 51 | 'api_key': 'KEY', 52 | 'api_secret': 'SECRET' 53 | }) 54 | config.read( candidates ) 55 | 56 | websocket_url = config.get('basebit', 'websocket_url') 57 | username = config.get('basebit', 'username') 58 | password = config.get('basebit', 'password') 59 | buy_fee = int(config.get('basebit', 'buy_fee')) 60 | sell_fee = int(config.get('basebit', 'sell_fee')) 61 | api_key = config.get('basebit', 'api_key') 62 | api_secret = config.get('basebit', 'api_secret') 63 | broker_id = config.getint('basebit', 'broker_id') 64 | dest_market = config.get('basebit', 'dest_market') 65 | 66 | print 'websocket_url:', websocket_url 67 | print 'username:', username 68 | print 'buy_fee:', buy_fee 69 | print 'sell_fee:', sell_fee 70 | 71 | arbitrator = BlinkTradeArbitrator(broker_id, username,password,websocket_url, dest_market ) 72 | arbitrator.connect() 73 | 74 | arbitrator.signal_order.connect(send_order_to_basebit) 75 | 76 | while True: 77 | try: 78 | sleep(10) 79 | 80 | if arbitrator.is_connected(): 81 | arbitrator.send_testRequest() 82 | else: 83 | try: 84 | arbitrator.reconnect() 85 | except HandshakeError,e: 86 | continue 87 | 88 | try: 89 | raw_data = urllib2.urlopen('http://www.basebit.com.br/book-BTC_BRL').read() 90 | except Exception: 91 | print 'ERROR RETRIEVING ORDER BOOK' 92 | continue 93 | 94 | bids_asks = [] 95 | try: 96 | bids_asks = json.loads(raw_data) 97 | except Exception : 98 | pass 99 | 100 | if bids_asks: 101 | ask_list = [ [ int(float(o['price']) * 1e8 * (1. + sell_fee) ) , int(o['quantity'] * 1e8) ] for o in bids_asks['result']['asks'] ] 102 | bid_list = [ [ int(float(o['price']) * 1e8 * (1. + buy_fee) ) , int(o['quantity'] * 1e8) ] for o in bids_asks['result']['bids'] ] 103 | arbitrator.process_ask_list(ask_list) 104 | arbitrator.process_bid_list(bid_list) 105 | except urllib2.URLError as e: 106 | print datetime.datetime.now(), e 107 | 108 | except KeyboardInterrupt: 109 | arbitrator.cancel_all_orders() 110 | print 'wait....' 111 | sleep(5) 112 | 113 | arbitrator.close() 114 | break 115 | 116 | main() 117 | 118 | -------------------------------------------------------------------------------- /arbitrage/bitfinex.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import urllib2 4 | from time import sleep 5 | import json 6 | 7 | from arbitrator import BlinkTradeArbitrator 8 | 9 | import datetime 10 | import hmac 11 | import hashlib 12 | import ConfigParser 13 | from ws4py.exc import HandshakeError 14 | 15 | BITFINEX_API_KEY = 'XXXX' 16 | BITFINEX_API_SECRET = 'YYYY' 17 | 18 | def send_order_to_bitfinex(sender, order): 19 | nonce = datetime.datetime.now().strftime('%s') 20 | message = 'sendorder' + str(BITFINEX_API_KEY) + str(nonce) 21 | signature = hmac.new(BITFINEX_API_SECRET, msg=message, digestmod=hashlib.sha256).hexdigest().upper() 22 | 23 | post_params = { 24 | 'key': BITFINEX_API_KEY, 25 | 'sign': signature, 26 | 'pair': 'btc_brl', 27 | 'volume': float(order['OrderQty']/1.e8), 28 | 'price': float( order['Price'] / 1.e8) 29 | } 30 | 31 | if order['Side'] == '1': 32 | post_params['type'] = 'buy' 33 | elif order['Side'] == '2': 34 | post_params['type'] = 'sell' 35 | 36 | print datetime.datetime.now(), 'POST https://api.bitfinex.com/v1/tapi/' + message, str(post_params) 37 | 38 | def main(): 39 | candidates = ['arbitrage.ini', 'bitfinex.ini' ] 40 | if len(sys.argv) > 1: 41 | candidates.append(sys.argv[1]) 42 | 43 | 44 | config = ConfigParser.SafeConfigParser({ 45 | 'websocket_url': 'wss://127.0.0.1/trade/', 46 | 'username': '', 47 | 'password': '', 48 | 'buy_fee': 0, 49 | 'sell_fee': 0, 50 | 'api_key': 'KEY', 51 | 'api_secret': 'SECRET' 52 | }) 53 | config.read( candidates ) 54 | 55 | websocket_url = config.get('bitfinex', 'websocket_url') 56 | username = config.get('bitfinex', 'username') 57 | password = config.get('bitfinex', 'password') 58 | buy_fee = int(config.get('bitfinex', 'buy_fee')) 59 | sell_fee = int(config.get('bitfinex', 'sell_fee')) 60 | api_key = config.get('bitfinex', 'api_key') 61 | api_secret = config.get('bitfinex', 'api_secret') 62 | broker_id = config.getint('bitfinex', 'broker_id') 63 | dest_market = config.get('bitfinex', 'dest_market') 64 | 65 | arbitrator = BlinkTradeArbitrator(broker_id, username,password,websocket_url, dest_market) 66 | arbitrator.connect() 67 | 68 | arbitrator.signal_order.connect(send_order_to_bitfinex) 69 | 70 | while True: 71 | try: 72 | sleep(1) 73 | 74 | if arbitrator.is_connected(): 75 | arbitrator.send_testRequest() 76 | else: 77 | try: 78 | arbitrator.reconnect() 79 | except HandshakeError,e: 80 | continue 81 | 82 | try: 83 | raw_data = urllib2.urlopen('https://api.bitfinex.com/v1/book/BTCUSD').read() 84 | except Exception: 85 | print 'ERROR RETRIEVING ORDER BOOK' 86 | continue 87 | 88 | 89 | bids_asks = [] 90 | try: 91 | bids_asks = json.loads(raw_data) 92 | except Exception : 93 | pass 94 | 95 | if bids_asks: 96 | bid_list = [ [ int(float(o['price']) * 1e8 * (1. + buy_fee) ) , int( float(o['amount']) * 1e8) ] for o in bids_asks['bids'] ] 97 | ask_list = [ [ int(float(o['price']) * 1e8 * (1. + sell_fee) ) , int( float(o['amount']) * 1e8) ] for o in bids_asks['asks'] ] 98 | arbitrator.process_ask_list(ask_list) 99 | arbitrator.process_bid_list(bid_list) 100 | except urllib2.URLError as e: 101 | print datetime.datetime.now(), e 102 | 103 | except KeyboardInterrupt: 104 | arbitrator.cancel_all_orders() 105 | print 'wait....' 106 | sleep(5) 107 | arbitrator.close() 108 | break 109 | 110 | main() 111 | 112 | -------------------------------------------------------------------------------- /arbitrage/bitinvest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import urllib2 4 | from time import sleep 5 | import json 6 | 7 | from arbitrator import BlinkTradeArbitrator 8 | 9 | import datetime 10 | import hmac 11 | import hashlib 12 | import subprocess 13 | import ConfigParser 14 | from ws4py.exc import HandshakeError 15 | 16 | 17 | BINVEST_API_KEY = 'XXXX' 18 | BINVEST_API_SECRET = 'YYYY' 19 | 20 | def send_order_to_BINVEST(sender, order): 21 | nonce = datetime.datetime.now().strftime('%s') 22 | message = 'sendorder' + str(BINVEST_API_KEY) + str(nonce) 23 | signature = hmac.new(BINVEST_API_SECRET, msg=message, digestmod=hashlib.sha256).hexdigest().upper() 24 | 25 | post_params = { 26 | 'key': BINVEST_API_KEY, 27 | 'sign': signature, 28 | 'pair': 'btc_brl', 29 | 'volume': float(order['OrderQty']/1.e8), 30 | 'price': float( order['Price'] / 1.e8) 31 | } 32 | 33 | if msg['Side'] == '1': 34 | post_params['type'] = 'buy' 35 | elif msg['Side'] == '2': 36 | post_params['type'] = 'sell' 37 | 38 | print datetime.datetime.now(), 'POST https://api.bitinvest.com.br/tapi/' + message, str(post_params) 39 | 40 | def main(): 41 | candidates = ['arbitrage.ini', 'basebit.ini' ] 42 | if len(sys.argv) > 1: 43 | candidates.append(sys.argv[1]) 44 | 45 | 46 | config = ConfigParser.SafeConfigParser({ 47 | 'websocket_url': 'wss://127.0.0.1/trade/', 48 | 'username': '', 49 | 'password': '', 50 | 'buy_fee': 0, 51 | 'sell_fee': 0, 52 | 'api_key': 'KEY', 53 | 'api_secret': 'SECRET', 54 | 'subscription_api_key':'api_key' 55 | }) 56 | config.read( candidates ) 57 | 58 | websocket_url = config.get('bitinvest', 'websocket_url') 59 | username = config.get('bitinvest', 'username') 60 | password = config.get('bitinvest', 'password') 61 | buy_fee = int(config.get('bitinvest', 'buy_fee')) 62 | sell_fee = int(config.get('bitinvest', 'sell_fee')) 63 | api_key = config.get('bitinvest', 'api_key') 64 | api_secret = config.get('bitinvest', 'api_secret') 65 | subscription_api_key = config.get('bitinvest', 'subscription_api_key') 66 | broker_id = config.getint('bitinvest', 'broker_id') 67 | dest_market = config.get('bitinvest', 'dest_market') 68 | 69 | 70 | arbitrator = BlinkTradeArbitrator(broker_id, username,password,websocket_url, dest_market) 71 | arbitrator.connect() 72 | 73 | arbitrator.signal_order.connect(send_order_to_BINVEST) 74 | 75 | while True: 76 | try: 77 | sleep(15) 78 | if arbitrator.is_connected(): 79 | arbitrator.send_testRequest() 80 | else: 81 | try: 82 | arbitrator.reconnect() 83 | except HandshakeError,e: 84 | continue 85 | 86 | try: 87 | # something wrong with urllib2 or bitinvest servers. 88 | #raw_data = urllib2.urlopen('https://api.bitinvest.com.br/exchange/orderbook?subscription-key=' + subscription_api_key).read() 89 | 90 | # curl works. I know, this is ugly, but it works 91 | api_url = 'https://api.bitinvest.com.br/exchange/orderbook?subscription-key=' + subscription_api_key 92 | raw_data = subprocess.check_output( ['curl', api_url ] ) 93 | except Exception: 94 | print 'ERROR RETRIEVING ORDER BOOK' 95 | continue 96 | 97 | bids_asks = [] 98 | try: 99 | bids_asks = json.loads(raw_data) 100 | except Exception : 101 | pass 102 | 103 | if bids_asks: 104 | ask_list = [ [ int(float(o[0]) * 1e8 * (1. + sell_fee) ) , int(o[1] * 1e8) ] for o in bids_asks['asks'] ] 105 | bid_list = [ [ int(float(o[0]) * 1e8 * (1. + buy_fee) ) , int(o[1] * 1e8) ] for o in bids_asks['bids'] ] 106 | arbitrator.process_ask_list(ask_list) 107 | arbitrator.process_bid_list(bid_list) 108 | except urllib2.URLError as e: 109 | print datetime.datetime.now(), e 110 | 111 | except KeyboardInterrupt: 112 | arbitrator.cancel_all_orders() 113 | print 'wait....' 114 | sleep(5) 115 | arbitrator.close() 116 | break 117 | 118 | main() 119 | 120 | -------------------------------------------------------------------------------- /arbitrage/bitstamp.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | import logging 5 | import ConfigParser 6 | 7 | ROOT_PATH = os.path.abspath( os.path.join(os.path.dirname(__file__), "../../")) 8 | sys.path.insert( 0, os.path.join(ROOT_PATH, 'libs')) 9 | 10 | import pusherclient 11 | import json 12 | 13 | import datetime 14 | import hmac 15 | import hashlib 16 | from arbitrator import BlinkTradeArbitrator 17 | from time import sleep 18 | from ws4py.exc import HandshakeError 19 | 20 | 21 | class BitStampClient(object): 22 | def __init__(self, broker_id,username,password,websocket_url, bid_fee=0., ask_fee=0., api_key=None, api_secret=None, dest_market='BTCUSD'): 23 | self.api_key = api_key 24 | self.api_secret = api_secret 25 | self.bid_fee = bid_fee 26 | self.ask_fee = ask_fee 27 | 28 | self.broker_id = broker_id 29 | self.username = username 30 | self.password = password 31 | self.websocket_url = websocket_url 32 | 33 | self.pusher = pusherclient.Pusher(key= 'de504dc5763aeef9ff52', log_level=logging.ERROR) 34 | self.pusher.connection.bind('pusher:connection_established', self.on_bitstamp_connect_handler) 35 | self.pusher.connection.bind('pusher:connection_failed', self.on_bitstamp_connect_failed_handler) 36 | 37 | self.arbitrator = BlinkTradeArbitrator( self.broker_id, self.username,self.password,self.websocket_url, dest_market) 38 | self.arbitrator.signal_order.connect(self.on_send_order_to_bitstamp) 39 | self.arbitrator.signal_logged.connect(self.on_blinktrade_logged) 40 | self.arbitrator.signal_disconnected.connect(self.on_blinktrade_discconnected) 41 | self.arbitrator.signal_connected.connect(self.on_blinktrade_connected) 42 | 43 | 44 | def on_blinktrade_discconnected(self, sender, code_reason): 45 | print datetime.datetime.now(), 'CLOSED', 'websocket closed. code:', code_reason[0], 'reason', code_reason[1] 46 | 47 | def on_blinktrade_logged(self, sender, data): 48 | print 'logged to blinktrade' 49 | self.arbitrator.cancel_all_orders() 50 | 51 | def on_blinktrade_connected(self, sender, data): 52 | print 'Connected to blinktrade' 53 | self.arbitrator.send_testRequest() 54 | 55 | def connect(self): 56 | print 'connecting....' 57 | self.arbitrator.connect() 58 | self.pusher.connect() 59 | 60 | def cancel_all_orders(self): 61 | if self.arbitrator.is_logged(): 62 | self.arbitrator.cancel_all_orders() 63 | 64 | def keep_alive(self): 65 | if self.arbitrator.is_connected(): 66 | self.arbitrator.send_testRequest() 67 | 68 | def on_bitstamp_connect_failed_handler(self, data): 69 | print 'Disconnected from bitstamp. Trying to reconnect within 10 minutes' 70 | if self.arbitrator.is_connected(): 71 | self.arbitrator.cancel_all_orders() 72 | self.pusher.connect() # reconnect to pusher 73 | 74 | def on_bitstamp_connect_handler(self, data): 75 | print 'connected to bitstamp' 76 | channel = self.pusher.subscribe('order_book') 77 | channel.bind('data', self.on_bitstamp_order_book_handler ) 78 | 79 | 80 | def on_bitstamp_order_book_handler(self, data): 81 | if not self.arbitrator.is_logged(): 82 | return 83 | 84 | data = json.loads(data) 85 | bid_list = [ [ int(float(usd)*1e8 * (1. - self.bid_fee) ), int(float(btc) * 1e8) ] for usd,btc in data['bids'] ] 86 | ask_list = [ [ int(float(usd)*1e8 * (1. + self.ask_fee) ), int(float(btc) * 1e8) ] for usd,btc in data['asks'] ] 87 | self.arbitrator.process_ask_list(ask_list) 88 | self.arbitrator.process_bid_list(bid_list) 89 | 90 | 91 | def on_send_order_to_bitstamp(self, sender, msg): 92 | nonce = datetime.datetime.now().strftime('%s') 93 | message = str(nonce) + '.blinktrade.' + str(self.api_key) 94 | signature = hmac.new(self.api_secret, msg=message, digestmod=hashlib.sha256).hexdigest().upper() 95 | 96 | post_params = { 97 | 'key': self.api_key, 98 | 'signature': signature, 99 | 'nonce': nonce, 100 | 'amount': float(msg['LastShares']/1.e8), 101 | 'price': float( msg['Price'] / 1.e8) 102 | } 103 | 104 | if msg['Side'] == '1': 105 | print datetime.datetime.now(), 'POST https://www.bitstamp.net/api/sell/', str(post_params) 106 | elif msg['Side'] == '2': 107 | print datetime.datetime.now(), 'POST https://www.bitstamp.net/api/buy/', str(post_params) 108 | 109 | 110 | def main(): 111 | candidates = ['arbitrage.ini', 'bitstamp.ini' ] 112 | if len(sys.argv) > 1: 113 | candidates.append(sys.argv[1]) 114 | 115 | 116 | config = ConfigParser.SafeConfigParser({ 117 | 'websocket_url': 'wss://127.0.0.1/trade/', 118 | 'broker_id': 5, 119 | 'username': '', 120 | 'password': '', 121 | 'buy_fee': 0, 122 | 'sell_fee': 0, 123 | 'api_key': 'KEY', 124 | 'api_secret': 'SECRET' 125 | }) 126 | config.read( candidates ) 127 | 128 | websocket_url = config.get('bitstamp', 'websocket_url') 129 | broker_id = config.getint('bitstamp', 'broker_id') 130 | username = config.get('bitstamp', 'username') 131 | password = config.get('bitstamp', 'password') 132 | buy_fee = float(config.get('bitstamp', 'buy_fee')) 133 | sell_fee = float(config.get('bitstamp', 'sell_fee')) 134 | api_key = config.get('bitstamp', 'api_key') 135 | api_secret = config.get('bitstamp', 'api_secret') 136 | broker_id = config.getint('bitstamp', 'broker_id') 137 | dest_market = config.get('bitstamp', 'dest_market') 138 | 139 | print 'websocket_url:', websocket_url 140 | print 'broker_id:', broker_id 141 | print 'username:', username 142 | print 'buy_fee:', buy_fee 143 | print 'sell_fee:', sell_fee 144 | 145 | bitstamp_blinktrade_arbitrator = BitStampClient(broker_id,username,password,websocket_url,buy_fee,sell_fee,api_key,api_secret, dest_market) 146 | bitstamp_blinktrade_arbitrator.connect() 147 | 148 | while True: 149 | try: 150 | sleep(5) 151 | 152 | if bitstamp_blinktrade_arbitrator.arbitrator.is_connected(): 153 | bitstamp_blinktrade_arbitrator.keep_alive() 154 | else: 155 | try: 156 | bitstamp_blinktrade_arbitrator.arbitrator.reconnect() 157 | except HandshakeError,e: 158 | continue 159 | 160 | except KeyboardInterrupt: 161 | break 162 | except Exception,e: 163 | print e 164 | break 165 | 166 | bitstamp_blinktrade_arbitrator.cancel_all_orders() 167 | 168 | main() 169 | 170 | -------------------------------------------------------------------------------- /arbitrage/client.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from ws4py.client.threadedclient import WebSocketClient 4 | import json 5 | import time 6 | 7 | from pyblinktrade.signals import Signal 8 | 9 | class BitExThreadedClient(WebSocketClient): 10 | signal_heartbeat = Signal() 11 | signal_logged = Signal() 12 | signal_error_login = Signal() 13 | signal_execution_report = Signal() 14 | signal_balance = Signal() # U3 15 | signal_security_list = Signal() # y 16 | signal_news = Signal() # B 17 | signal_error = Signal() #ERROR 18 | 19 | signal_deposit_refresh = Signal() 20 | signal_deposit_response = Signal() 21 | signal_process_deposit_response = Signal() 22 | 23 | signal_verify_customer_response = Signal() 24 | signal_verify_customer_update = Signal() 25 | 26 | 27 | signal_connection_open = Signal() 28 | signal_connection_closed = Signal() 29 | 30 | signal_book_bid_clear = Signal() 31 | signal_book_bid_new_order = Signal() 32 | signal_book_bid_update_order = Signal() 33 | signal_book_bid_delete_order = Signal() 34 | signal_book_bid_delete_thru = Signal() 35 | signal_book_offer_clear = Signal() 36 | signal_book_offer_new_order = Signal() 37 | signal_book_offer_update_order = Signal() 38 | signal_book_offer_delete_order = Signal() 39 | signal_book_offer_delete_thru = Signal() 40 | 41 | signal_trade_clear = Signal() 42 | signal_trade = Signal() 43 | 44 | signal_recv = Signal() 45 | signal_send = Signal() 46 | 47 | is_logged = False 48 | is_connected = False 49 | 50 | def closed(self, code, reason): 51 | print 'BitExThreadedClient::closed' 52 | self.is_connected = False 53 | self.is_logged = False 54 | self.signal_connection_closed(self, (code, reason)) 55 | 56 | def opened(self): 57 | print "opened" 58 | self.is_connected = True 59 | self.is_logged = False 60 | self.signal_connection_open(self) 61 | 62 | def send(self, payload, binary=False): 63 | print "sending: " + str(payload) 64 | if self.is_connected: 65 | self.signal_send(self, payload) 66 | super(BitExThreadedClient, self).send(payload, binary) 67 | 68 | def login(self, broker_id, user, password): 69 | print "login: " + str(broker_id) + " " + user + " " + password 70 | if not user or not password: 71 | raise ValueError('Invalid parameters') 72 | 73 | loginMsg = { 74 | 'UserReqID': 'initial', 75 | 'MsgType' : 'BE', 76 | 'BrokerID': broker_id, 77 | 'Username': user, 78 | 'Password':password, 79 | 'UserReqTyp': '1', 80 | 'FingerPrint': 9999 81 | } 82 | self.send(json.dumps(loginMsg)) 83 | 84 | def testRequest(self, request_id=None): 85 | if request_id: 86 | self.send(json.dumps({'MsgType': '1', 'TestReqID': request_id })) 87 | else: 88 | self.send(json.dumps({'MsgType': '1', 'TestReqID': int(time.time()*1000)})) 89 | 90 | def verifyCustomer(self , client_id, verify, verification_data, opt_request_id=None): 91 | if not opt_request_id: 92 | opt_request_id = random.randint(1,10000000) 93 | 94 | msg = { 95 | 'MsgType': 'B8', 96 | 'VerifyCustomerReqID': opt_request_id, 97 | 'ClientID': client_id, 98 | 'Verify': verify, 99 | 'VerificationData': verification_data 100 | } 101 | 102 | self.send(json.dumps(msg)) 103 | 104 | return opt_request_id 105 | 106 | 107 | def processDeposit(self, 108 | action, 109 | opt_request_id = None, 110 | opt_secret=None, 111 | opt_depositId=None, 112 | opt_reasonId=None, 113 | opt_reason=None, 114 | opt_amount=None, 115 | opt_percent_fee=None, 116 | opt_fixed_fee=None): 117 | if not opt_request_id: 118 | opt_request_id = random.randint(1,10000000) 119 | 120 | msg = { 121 | 'MsgType': 'B0', 122 | 'ProcessDepositReqID': opt_request_id, 123 | 'Action': action 124 | } 125 | 126 | if opt_secret: 127 | msg['Secret'] = opt_secret 128 | 129 | if opt_depositId: 130 | msg['DepositID'] = opt_depositId 131 | 132 | if opt_reasonId: 133 | msg['ReasonID'] = opt_reasonId 134 | 135 | if opt_reason: 136 | msg['Reason'] = opt_reason 137 | 138 | if opt_amount: 139 | msg['Amount'] = opt_amount 140 | 141 | if opt_percent_fee: 142 | msg['PercentFee'] = opt_percent_fee 143 | 144 | if opt_fixed_fee: 145 | msg['FixedFee'] = opt_fixed_fee 146 | 147 | self.send(json.dumps(msg)) 148 | 149 | return opt_request_id 150 | 151 | def requestBalances(self, request_id = None, client_id = None): 152 | if not request_id: 153 | request_id = random.randint(1,10000000) 154 | msg = { 155 | 'MsgType': 'U2', 156 | 'BalanceReqID': request_id 157 | } 158 | if client_id: 159 | msg['ClientID'] = client_id 160 | self.send(json.dumps(msg)) 161 | 162 | return request_id 163 | 164 | def requestMarketData(self, request_id, symbols, entry_types, subscription_type='1', market_depth=0 ,update_type = '1'): 165 | if not symbols or not entry_types: 166 | raise ValueError('Invalid parameters') 167 | 168 | subscribe_msg = { 169 | 'MsgType' : 'V', 170 | 'MDReqID': request_id, 171 | 'SubscriptionRequestType': subscription_type, 172 | 'MarketDepth': market_depth, 173 | 'MDUpdateType': update_type, # 174 | 'MDEntryTypes': entry_types, # bid , offer, trade 175 | 'Instruments': symbols 176 | } 177 | self.send(json.dumps(subscribe_msg)) 178 | 179 | return request_id 180 | 181 | def sendLimitedBuyOrder(self, symbol, qty, price, clientOrderId ): 182 | if not symbol or not qty or not qty or not price or not clientOrderId: 183 | raise ValueError('Invalid parameters') 184 | 185 | if qty <= 0 or price <= 0: 186 | raise ValueError('Invalid qty or price') 187 | 188 | msg = { 189 | 'MsgType': 'D', 190 | 'ClOrdID': str(clientOrderId), 191 | 'Symbol': symbol, 192 | 'Side': '1', 193 | 'OrdType': '2', 194 | 'Price': price, 195 | 'OrderQty': qty 196 | } 197 | self.send(json.dumps(msg)) 198 | 199 | def sendLimitedSellOrder(self, symbol, qty, price, clientOrderId ): 200 | if not symbol or not qty or not qty or not price or not clientOrderId: 201 | raise ValueError('Invalid parameters') 202 | 203 | if qty <= 0 or price <= 0: 204 | raise ValueError('Invalid qty or price') 205 | 206 | msg = { 207 | 'MsgType': 'D', 208 | 'ClOrdID': str(clientOrderId), 209 | 'Symbol': symbol, 210 | 'Side': '2', 211 | 'OrdType': '2', 212 | 'Price': price, 213 | 'OrderQty': qty 214 | } 215 | self.send(json.dumps(msg)) 216 | 217 | def sendMsg(self, msg ): 218 | self.send(json.dumps(msg)) 219 | 220 | def received_message(self, message): 221 | msg = json.loads(str(message)) 222 | 223 | self.signal_recv(self, msg) 224 | 225 | if msg['MsgType'] == '0': 226 | self.signal_heartbeat(self, msg) 227 | elif msg['MsgType'] == 'BF': 228 | if msg['UserStatus'] == 1: 229 | self.is_logged = True 230 | self.signal_logged(self, msg) 231 | else: 232 | self.signal_error_login(self, msg) 233 | elif msg['MsgType'] == '8': 234 | self.signal_execution_report(self, msg) 235 | 236 | elif msg['MsgType'] == 'U3': 237 | self.signal_balance(self, msg) 238 | 239 | elif msg['MsgType'] == 'y': 240 | self.signal_security_list(self, msg) 241 | 242 | elif msg['MsgType'] == 'B': 243 | self.signal_news(self, msg) 244 | 245 | elif msg['MsgType'] == 'ERROR': 246 | self.signal_error(self, msg) 247 | 248 | elif msg['MsgType'] == 'B1': #Process Deposit Response 249 | self.signal_process_deposit_response(self, msg) 250 | 251 | elif msg['MsgType'] == 'B9': #Verification Customer Response 252 | self.signal_verify_customer_response(self, msg) 253 | 254 | elif msg['MsgType'] == 'B11': #Verification Customer Update 255 | self.signal_verify_customer_update(self, msg) 256 | 257 | elif msg['MsgType'] == 'U19': #Deposit Response 258 | self.signal_deposit_response(self, msg) 259 | 260 | elif msg['MsgType'] == 'U23': #Deposit Refresh 261 | self.signal_deposit_refresh(self, msg) 262 | 263 | elif msg['MsgType'] == 'X': # Market Data Incremental Refresh 264 | if msg['MDBkTyp'] == '3': # Order Depth 265 | for entry in msg['MDIncGrp']: 266 | if entry['MDEntryType'] == '0': 267 | if entry['MDUpdateAction'] == '0': 268 | self.signal_book_bid_new_order(self, entry ) 269 | elif entry['MDUpdateAction'] == '1': 270 | self.signal_book_bid_update_order(self, entry ) 271 | elif entry['MDUpdateAction'] == '2': 272 | self.signal_book_bid_delete_order(self, entry ) 273 | elif entry['MDUpdateAction'] == '3': 274 | self.signal_book_bid_delete_thru(self, entry ) 275 | elif entry['MDEntryType'] == '1': 276 | if entry['MDUpdateAction'] == '0': 277 | self.signal_book_offer_new_order(self, entry ) 278 | elif entry['MDUpdateAction'] == '1': 279 | self.signal_book_offer_update_order(self, entry ) 280 | elif entry['MDUpdateAction'] == '2': 281 | self.signal_book_offer_delete_order(self, entry ) 282 | elif entry['MDUpdateAction'] == '3': 283 | self.signal_book_offer_delete_thru(self, entry ) 284 | elif entry['MDEntryType'] == '2': 285 | self.signal_trade(self, entry ) 286 | 287 | elif msg['MsgType'] == 'W': # Market Data Refresh 288 | if msg['MarketDepth'] != 1 :# Has Market Depth 289 | self.signal_book_bid_clear(self, "") 290 | self.signal_book_offer_clear(self, "") 291 | self.signal_trade_clear(self, "") 292 | 293 | for entry in msg['MDFullGrp']: 294 | if entry['MDEntryType'] == '0': 295 | self.signal_book_bid_new_order(self, entry ) 296 | elif entry['MDEntryType'] == '1': 297 | self.signal_book_offer_new_order(self, entry ) 298 | elif entry['MDEntryType'] == '2': 299 | self.signal_trade(self, entry ) 300 | 301 | 302 | 303 | if __name__ == '__main__': 304 | ws = BitExThreadedClient('ws://localhost:8445/') 305 | try: 306 | def on_login(sender, msg): 307 | print 'received ' , msg 308 | ws.testRequest() 309 | ws.requestMarketData( 'md', ['BTCUSD'], ['0','1', '2'] ) 310 | 311 | 312 | def on_message(sender, msg): 313 | print 'received ' , msg 314 | print '' 315 | 316 | ws.signal_logged.connect(on_login) 317 | ws.signal_recv.connect(on_message) 318 | 319 | print 'connecting' 320 | ws.connect() 321 | time.sleep(1) 322 | ws.login(5, 'user','abc12345') 323 | 324 | ws.run_forever() 325 | except KeyboardInterrupt: 326 | ws.close() 327 | -------------------------------------------------------------------------------- /arbitrage/hitbtc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import urllib2 4 | from time import sleep 5 | import json 6 | 7 | from arbitrator import BlinkTradeArbitrator 8 | 9 | import datetime 10 | import hmac 11 | import hashlib 12 | import ConfigParser 13 | from ws4py.exc import HandshakeError 14 | 15 | 16 | HITBTC_API_KEY = 'XXXX' 17 | HITBTC_API_SECRET = 'YYYY' 18 | 19 | def send_order(sender, order): 20 | nonce = datetime.datetime.now().strftime('%s') 21 | message = 'sendorder' + str(HITBTC_API_KEY) + str(nonce) 22 | signature = hmac.new(HITBTC_API_SECRET, msg=message, digestmod=hashlib.sha256).hexdigest().upper() 23 | 24 | post_params = { 25 | 'key': HITBTC_API_KEY, 26 | 'sign': signature, 27 | 'pair': 'btc_brl', 28 | 'volume': float(order['OrderQty']/1.e8), 29 | 'price': float( order['Price'] / 1.e8) 30 | } 31 | 32 | if order['Side'] == '1': 33 | post_params['type'] = 'buy' 34 | elif order['Side'] == '2': 35 | post_params['type'] = 'sell' 36 | 37 | print datetime.datetime.now(), 'POST https://api.hitbtc.com/v1/tapi/' + message, str(post_params) 38 | 39 | def main(): 40 | candidates = ['arbitrage.ini', 'hitbtc.ini' ] 41 | if len(sys.argv) > 1: 42 | candidates.append(sys.argv[1]) 43 | 44 | 45 | config = ConfigParser.SafeConfigParser({ 46 | 'websocket_url': 'wss://127.0.0.1/trade/', 47 | 'username': '', 48 | 'password': '', 49 | 'buy_fee': 0, 50 | 'sell_fee': 0, 51 | 'api_key': 'KEY', 52 | 'api_secret': 'SECRET' 53 | }) 54 | config.read( candidates ) 55 | 56 | websocket_url = config.get('hitbtc', 'websocket_url') 57 | username = config.get('hitbtc', 'username') 58 | password = config.get('hitbtc', 'password') 59 | buy_fee = int(config.get('hitbtc', 'buy_fee')) 60 | sell_fee = int(config.get('hitbtc', 'sell_fee')) 61 | api_key = config.get('hitbtc', 'api_key') 62 | api_secret = config.get('hitbtc', 'api_secret') 63 | broker_id = config.getint('hitbtc', 'broker_id') 64 | dest_market = config.get('hitbtc', 'dest_market') 65 | 66 | 67 | arbitrator = BlinkTradeArbitrator(broker_id, username,password,websocket_url, dest_market) 68 | arbitrator.connect() 69 | 70 | arbitrator.signal_order.connect(send_order) 71 | 72 | while True: 73 | try: 74 | sleep(1) 75 | if arbitrator.is_connected(): 76 | arbitrator.send_testRequest() 77 | else: 78 | try: 79 | arbitrator.reconnect() 80 | except HandshakeError,e: 81 | continue 82 | 83 | try: 84 | raw_data = urllib2.urlopen('https://api.hitbtc.com/api/1/public/BTCUSD/orderbook?format_price=number&format_amount=number').read() 85 | except Exception: 86 | print 'ERROR RETRIEVING ORDER BOOK' 87 | continue 88 | 89 | 90 | bids_asks = [] 91 | try: 92 | bids_asks = json.loads(raw_data) 93 | except Exception : 94 | pass 95 | 96 | if bids_asks: 97 | bid_list = [ [ int(float(o[0]) * 1e8 * (1. + buy_fee) ) , int( float(o[1]) * 1e8) ] for o in bids_asks['bids'] ] 98 | ask_list = [ [ int(float(o[0]) * 1e8 * (1. + sell_fee) ) , int( float(o[1]) * 1e8) ] for o in bids_asks['asks'] ] 99 | arbitrator.process_ask_list(ask_list) 100 | arbitrator.process_bid_list(bid_list) 101 | except urllib2.URLError as e: 102 | print datetime.datetime.now(), e 103 | 104 | except KeyboardInterrupt: 105 | arbitrator.cancel_all_orders() 106 | print 'wait....' 107 | sleep(5) 108 | arbitrator.close() 109 | break 110 | 111 | main() 112 | 113 | -------------------------------------------------------------------------------- /arbitrage/itbit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import urllib2 4 | from time import sleep 5 | import json 6 | 7 | from arbitrator import BlinkTradeArbitrator 8 | 9 | import datetime 10 | import hmac 11 | import hashlib 12 | import ConfigParser 13 | from ws4py.exc import HandshakeError 14 | 15 | 16 | ITBIT_API_KEY = 'XXXX' 17 | ITBIT_API_SECRET = 'YYYY' 18 | 19 | def send_order(sender, order): 20 | nonce = datetime.datetime.now().strftime('%s') 21 | message = 'sendorder' + str(ITBIT_API_KEY) + str(nonce) 22 | signature = hmac.new(ITBIT_API_SECRET, msg=message, digestmod=hashlib.sha256).hexdigest().upper() 23 | 24 | post_params = { 25 | 'key': ITBIT_API_KEY, 26 | 'sign': signature, 27 | 'pair': 'btc_brl', 28 | 'volume': float(order['OrderQty']/1.e8), 29 | 'price': float( order['Price'] / 1.e8) 30 | } 31 | 32 | if order['Side'] == '1': 33 | post_params['type'] = 'buy' 34 | elif order['Side'] == '2': 35 | post_params['type'] = 'sell' 36 | 37 | print datetime.datetime.now(), 'POST https://api.hitbtc.com/v1/tapi/' + message, str(post_params) 38 | 39 | def main(): 40 | candidates = ['arbitrage.ini', 'itbit.ini' ] 41 | if len(sys.argv) > 1: 42 | candidates.append(sys.argv[1]) 43 | 44 | 45 | config = ConfigParser.SafeConfigParser({ 46 | 'websocket_url': 'wss://127.0.0.1/trade/', 47 | 'username': '', 48 | 'password': '', 49 | 'buy_fee': 0, 50 | 'sell_fee': 0, 51 | 'api_key': 'KEY', 52 | 'api_secret': 'SECRET' 53 | }) 54 | config.read( candidates ) 55 | 56 | websocket_url = config.get('itbit', 'websocket_url') 57 | username = config.get('itbit', 'username') 58 | password = config.get('itbit', 'password') 59 | buy_fee = float(config.get('itbit', 'buy_fee')) 60 | sell_fee = float(config.get('itbit', 'sell_fee')) 61 | api_key = config.get('itbit', 'api_key') 62 | api_secret = config.get('itbit', 'api_secret') 63 | broker_id = config.getint('itbit', 'broker_id') 64 | dest_market = config.get('itbit', 'dest_market') 65 | 66 | arbitrator = BlinkTradeArbitrator(broker_id,username,password,websocket_url, dest_market ) 67 | arbitrator.connect() 68 | 69 | arbitrator.signal_order.connect(send_order) 70 | 71 | while True: 72 | try: 73 | sleep(1) 74 | if arbitrator.is_connected(): 75 | arbitrator.send_testRequest() 76 | else: 77 | try: 78 | arbitrator.reconnect() 79 | except HandshakeError,e: 80 | continue 81 | 82 | try: 83 | raw_data = urllib2.urlopen('https://www.itbit.com/api/v2/markets/XBTUSD/orders').read() 84 | except Exception: 85 | print 'ERROR RETRIEVING ORDER BOOK' 86 | continue 87 | 88 | 89 | bids_asks = [] 90 | try: 91 | bids_asks = json.loads(raw_data) 92 | except Exception : 93 | pass 94 | 95 | if bids_asks: 96 | bid_list = [ [ int(float(o[0]) * 1e8 * (1. + buy_fee) ) , int( float(o[1]) * 1e8) ] for o in bids_asks['bids'] ] 97 | ask_list = [ [ int(float(o[0]) * 1e8 * (1. + sell_fee) ) , int( float(o[1]) * 1e8) ] for o in bids_asks['asks'] ] 98 | arbitrator.process_ask_list(ask_list) 99 | arbitrator.process_bid_list(bid_list) 100 | except urllib2.URLError as e: 101 | print datetime.datetime.now(), e 102 | 103 | except KeyboardInterrupt: 104 | arbitrator.cancel_all_orders() 105 | print 'wait....' 106 | sleep(5) 107 | arbitrator.close() 108 | break 109 | 110 | main() 111 | 112 | -------------------------------------------------------------------------------- /arbitrage/mb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import urllib2 4 | from time import sleep 5 | import json 6 | 7 | from arbitrator import BlinkTradeArbitrator 8 | 9 | import datetime 10 | import hmac 11 | import hashlib 12 | import ConfigParser 13 | from ws4py.exc import HandshakeError 14 | 15 | 16 | MB_API_KEY = 'XXXX' 17 | MB_API_SECRET = 'YYYY' 18 | 19 | def send_order_to_MB(sender, order): 20 | nonce = datetime.datetime.now().strftime('%s') 21 | message = 'sendorder' + str(MB_API_KEY) + str(nonce) 22 | signature = hmac.new(MB_API_SECRET, msg=message, digestmod=hashlib.sha256).hexdigest().upper() 23 | 24 | post_params = { 25 | 'key': MB_API_KEY, 26 | 'sign': signature, 27 | 'pair': 'btc_brl', 28 | 'volume': float(order['OrderQty']/1.e8), 29 | 'price': float( order['Price'] / 1.e8) 30 | } 31 | 32 | if msg['Side'] == '1': 33 | post_params['type'] = 'buy' 34 | elif msg['Side'] == '2': 35 | post_params['type'] = 'sell' 36 | 37 | print datetime.datetime.now(), 'POST https://www.mercadobitcoin.com.br/tapi/' + message, str(post_params) 38 | 39 | def main(): 40 | candidates = ['arbitrage.ini', 'mb.ini' ] 41 | if len(sys.argv) > 1: 42 | candidates.append(sys.argv[1]) 43 | 44 | 45 | config = ConfigParser.SafeConfigParser({ 46 | 'websocket_url': 'wss://127.0.0.1/trade/', 47 | 'username': '', 48 | 'password': '', 49 | 'buy_fee': 0, 50 | 'sell_fee': 0, 51 | 'api_key': 'KEY', 52 | 'api_secret': 'SECRET' 53 | }) 54 | config.read( candidates ) 55 | 56 | websocket_url = config.get('mb', 'websocket_url') 57 | username = config.get('mb', 'username') 58 | password = config.get('mb', 'password') 59 | buy_fee = int(config.get('mb', 'buy_fee')) 60 | sell_fee = int(config.get('mb', 'sell_fee')) 61 | api_key = config.get('mb', 'api_key') 62 | api_secret = config.get('mb', 'api_secret') 63 | broker_id = config.getint('mb', 'broker_id') 64 | dest_market = config.get('mb', 'dest_market') 65 | 66 | print 'websocket_url:', websocket_url 67 | print 'username:', username 68 | print 'buy_fee:', buy_fee 69 | print 'sell_fee:', sell_fee 70 | 71 | arbitrator = BlinkTradeArbitrator(broker_id, username,password,websocket_url, dest_market) 72 | arbitrator.connect() 73 | 74 | arbitrator.signal_order.connect(send_order_to_MB) 75 | 76 | while True: 77 | try: 78 | sleep(5) 79 | if arbitrator.is_connected(): 80 | arbitrator.send_testRequest() 81 | else: 82 | try: 83 | arbitrator.reconnect() 84 | except HandshakeError,e: 85 | continue 86 | 87 | try: 88 | raw_data = urllib2.urlopen('https://www.mercadobitcoin.com.br/api/orderbook/').read() 89 | except Exception: 90 | print 'ERROR RETRIEVING ORDER BOOK' 91 | continue 92 | 93 | bids_asks = [] 94 | try: 95 | bids_asks = json.loads(raw_data) 96 | except Exception : 97 | pass 98 | 99 | if bids_asks: 100 | ask_list = [ [ int(float(o[0]) * 1e8 * (1. + sell_fee) ) , int(o[1] * 1e8) ] for o in bids_asks['asks'] ] 101 | bid_list = [ [ int(float(o[0]) * 1e8 * (1. + buy_fee) ) , int(o[1] * 1e8) ] for o in bids_asks['bids'] ] 102 | arbitrator.process_ask_list(ask_list) 103 | arbitrator.process_bid_list(bid_list) 104 | 105 | except urllib2.URLError as e: 106 | print datetime.datetime.now(), e 107 | 108 | except KeyboardInterrupt: 109 | arbitrator.cancel_all_orders() 110 | print 'wait....' 111 | sleep(5) 112 | arbitrator.close() 113 | break 114 | 115 | main() 116 | 117 | -------------------------------------------------------------------------------- /arbitrage/order_book_processor.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.insert(0, '../../libs') 3 | 4 | from pyblinktrade.signals import Signal 5 | import datetime 6 | import time 7 | import random 8 | 9 | class OrderBookProcessor(): 10 | def __init__(self, side, symbol): 11 | self.side = side 12 | self.symbol = symbol 13 | self.orders_by_id = {} 14 | self.orders_by_price = {} 15 | self.orders_list_ordered_by_timestamp = [] 16 | 17 | self.send_new_order_signal = Signal() 18 | self.cancel_order_signal = Signal() 19 | 20 | def _get_order_by_price(self, price): 21 | if price in self.orders_by_price: 22 | return self.orders_by_price[price] 23 | return None 24 | 25 | def _send_new_order(self, price, volume): 26 | now = datetime.datetime.now() 27 | timestamp = time.mktime(now.timetuple())*1e3 + now.microsecond/1e3 28 | 29 | order_id = str(int(timestamp)) + str(int(random.random()*100000)) 30 | 31 | order = { 'id': order_id, 'price': price, 'vol': volume , 'ts': timestamp } 32 | self.orders_by_price[price] = order 33 | self.orders_by_id[order_id] = order 34 | 35 | self.orders_list_ordered_by_timestamp.append( order ) 36 | 37 | order_message = { 38 | 'MsgType': 'D', 39 | 'Symbol' : self.symbol, 40 | 'OrdType':'2', 41 | 'Price': int(price), 42 | 'OrderQty': int(volume), 43 | 'ClOrdID': str(order_id), 44 | 'Side': self.side, 45 | } 46 | self.send_new_order_signal( self, order_message) 47 | 48 | return order_id 49 | 50 | def _send_cancel_replace_order(self, order_id, new_volume): 51 | original_order = self.orders_by_id[order_id] 52 | 53 | original_volume = original_order['vol'] 54 | if original_volume != new_volume: 55 | self._cancel_order( original_order['id'] ) 56 | return self._send_new_order( original_order['price'], new_volume ) 57 | else: 58 | # nothing to do ... let's just update the current order timestamp. 59 | now = datetime.datetime.now() 60 | new_timestamp = time.mktime(now.timetuple())*1e3 + now.microsecond/1e3 61 | original_order['ts'] = new_timestamp 62 | 63 | pos = 0 64 | for order in self.orders_list_ordered_by_timestamp: 65 | if order['id'] == order_id: 66 | break 67 | pos += 1 68 | del self.orders_list_ordered_by_timestamp[pos] 69 | self.orders_list_ordered_by_timestamp.append( original_order ) 70 | 71 | def _get_last_timestamp(self): 72 | if not self.orders_list_ordered_by_timestamp: 73 | now = datetime.datetime.now() 74 | timestamp = time.mktime(now.timetuple())*1e3 + now.microsecond/1e3 75 | return timestamp 76 | return self.orders_list_ordered_by_timestamp[-1]['ts'] 77 | 78 | def _cancel_all_orders_prior_timestamp(self, timestamp): 79 | orders_to_cancel = [] 80 | for order in self.orders_list_ordered_by_timestamp: 81 | if order['ts'] <= timestamp: 82 | orders_to_cancel.append(order['id']) 83 | 84 | for order_id in orders_to_cancel: 85 | self._cancel_order( order_id ) 86 | 87 | def _cancel_order(self, order_id): 88 | original_order = self.orders_by_id[order_id] 89 | 90 | self.cancel_order_signal(self, { 'MsgType':'F', 'OrigClOrdID': str(order_id)} ) 91 | 92 | # find the order position 93 | pos = 0 94 | for order in self.orders_list_ordered_by_timestamp: 95 | if order['id'] == order_id: 96 | break 97 | pos += 1 98 | del self.orders_list_ordered_by_timestamp[pos] 99 | 100 | del self.orders_by_price[original_order['price']] 101 | del self.orders_by_id[original_order['id'] ] 102 | return True 103 | 104 | def process_order_list(self, order_list): 105 | bid_timestamp = self._get_last_timestamp() 106 | for o in order_list: 107 | order_volume = o[1] 108 | order_price = o[0] 109 | 110 | # get the order using the price 111 | order = self._get_order_by_price(order_price) 112 | if order: 113 | if not order_volume: 114 | self._cancel_order(order['id']) 115 | else: 116 | self._send_cancel_replace_order( order['id'], order_volume ) 117 | else: 118 | if order_price and order_volume: 119 | self._send_new_order(order_price, order_volume) 120 | self._cancel_all_orders_prior_timestamp(bid_timestamp) 121 | -------------------------------------------------------------------------------- /arbitrage/util.py: -------------------------------------------------------------------------------- 1 | def get_funded_entries(orders, balance, is_total_vol): 2 | total_vol_usd = 0 3 | total_vol_btc = 0 4 | funded_entries = [] 5 | for price_usd, size_btc in orders: 6 | vol_usd = (price_usd * size_btc) / 1e8 7 | previous_total_vol_btc = total_vol_btc 8 | previous_total_vol_usd = total_vol_usd 9 | total_vol_usd += vol_usd 10 | total_vol_btc += size_btc 11 | if is_total_vol: 12 | if total_vol_usd > balance: 13 | available_volume = balance - previous_total_vol_usd 14 | if available_volume: 15 | funded_entries.append([ price_usd, int( (float (available_volume) / float (price_usd)) * 1.e8) ]) 16 | break 17 | else: 18 | if total_vol_btc > balance: 19 | if balance-previous_total_vol_btc: 20 | funded_entries.append([ price_usd, int(balance-previous_total_vol_btc) ]) 21 | break 22 | funded_entries.append([ price_usd, size_btc ]) 23 | return funded_entries 24 | 25 | 26 | def aggregate_orders(order_list): 27 | res = [] 28 | for price, size in order_list: 29 | if res: 30 | if res[-1][0] == price: 31 | res[-1][1] += size 32 | else: 33 | res.append( [ price, size ] ) 34 | else: 35 | res.append( [ price, size ] ) 36 | return res 37 | -------------------------------------------------------------------------------- /bitcoind/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:xenial 2 | MAINTAINER Clebson Derivan 3 | 4 | ARG USER_ID 5 | ARG GROUP_ID 6 | 7 | ENV HOME /bitcoin 8 | 9 | # add user with specified (or default) user/group ids 10 | ENV USER_ID ${USER_ID:-1000} 11 | ENV GROUP_ID ${GROUP_ID:-1000} 12 | 13 | # add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added 14 | RUN groupadd -g ${GROUP_ID} bitcoin \ 15 | && useradd -u ${USER_ID} -g bitcoin -s /bin/bash -m -d /bitcoin bitcoin 16 | 17 | RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys C70EF1F0305A1ADB9986DBD8D46F45428842CE5E && \ 18 | echo "deb http://ppa.launchpad.net/bitcoin/bitcoin/ubuntu xenial main" > /etc/apt/sources.list.d/bitcoin.list 19 | 20 | RUN apt-get update && apt-get install -y --no-install-recommends \ 21 | bitcoind curl \ 22 | && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 23 | 24 | # grab gosu for easy step-down from root 25 | ENV GOSU_VERSION 1.7 26 | RUN set -x \ 27 | && apt-get update && apt-get install -y --no-install-recommends \ 28 | ca-certificates \ 29 | wget \ 30 | && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture)" \ 31 | && wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture).asc" \ 32 | && export GNUPGHOME="$(mktemp -d)" \ 33 | && gpg --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 \ 34 | && gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu \ 35 | && rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc \ 36 | && chmod +x /usr/local/bin/gosu \ 37 | && gosu nobody true \ 38 | && apt-get purge -y \ 39 | ca-certificates \ 40 | wget \ 41 | && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 42 | 43 | ADD ./bitcoind/bin /usr/local/bin 44 | 45 | EXPOSE 8332 8333 18332 18333 46 | 47 | WORKDIR /bitcoin 48 | 49 | COPY ./bitcoind/docker-entrypoint.sh /usr/local/bin/ 50 | ENTRYPOINT ["docker-entrypoint.sh"] 51 | 52 | CMD ["btc_oneshot"] 53 | -------------------------------------------------------------------------------- /bitcoind/bin/blocknotify: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | curl http://nginx/api/blocknotify/$1 3 | -------------------------------------------------------------------------------- /bitcoind/bin/btc_init: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | 5 | # This shouldn't be in the Dockerfile or containers built from the same image 6 | # will have the same credentials. 7 | if [ ! -e "$HOME/.bitcoin/bitcoin.conf" ]; then 8 | mkdir -p $HOME/.bitcoin 9 | 10 | echo "Creating bitcoin.conf" 11 | 12 | # Seed a random password for JSON RPC server 13 | cat < $HOME/.bitcoin/bitcoin.conf 14 | testnet=1 15 | disablewallet=${DISABLEWALLET:-1} 16 | printtoconsole=${PRINTTOCONSOLE:-1} 17 | rpcuser=${RPCUSER:-bitcoinrpc} 18 | rpcpassword=${RPCPASSWORD:-`dd if=/dev/urandom bs=33 count=1 2>/dev/null | base64`} 19 | 20 | walletnotify=/usr/local/bin/walletnotify %s 21 | blocknotify=/usr/local/bin/blocknotify %s 22 | 23 | paytxfee=${PAYTXFEE:-0.0001} 24 | 25 | EOF 26 | 27 | fi 28 | 29 | cat $HOME/.bitcoin/bitcoin.conf 30 | 31 | echo "Initialization completed successfully" 32 | -------------------------------------------------------------------------------- /bitcoind/bin/btc_oneshot: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | # Generate bitcoin.conf 6 | btc_init 7 | 8 | if [ $# -gt 0 ]; then 9 | args=("$@") 10 | else 11 | args=("-debug") 12 | fi 13 | 14 | exec bitcoind "${args[@]}" 15 | 16 | -------------------------------------------------------------------------------- /bitcoind/bin/walletnotify: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | curl http://nginx/api/walletnotify/$1 3 | -------------------------------------------------------------------------------- /bitcoind/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # first arg is `-f` or `--some-option` 5 | # or first arg is `something.conf` 6 | if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then 7 | set -- btc_oneshot "$@" 8 | fi 9 | 10 | # allow the container to be started with `--user` 11 | if [ "$1" = 'btc_oneshot' -a "$(id -u)" = '0' ]; then 12 | chown -R bitcoin . 13 | exec gosu bitcoin "$0" "$@" 14 | fi 15 | 16 | exec "$@" 17 | 18 | -------------------------------------------------------------------------------- /bitex.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | project_root_demo=/opt/bitex 3 | host_demo = bitcoindubai.ae 4 | trade_in_demo = ipc:///%(project_root)s/trade_in_api_testnet 5 | trade_pub_demo = ipc:///%(project_root)s/trade_pub_api_testnet 6 | 7 | [trade_demo] 8 | project_root=%(project_root_demo)s 9 | db_echo = False 10 | trade_in = %(trade_in_demo)s 11 | trade_pub = %(trade_pub_demo)s 12 | trade_log = %(project_root)s/logs/trade_testnet.log 13 | session_timeout_limit = 0 14 | test_mode = False 15 | dev_mode = False 16 | satoshi_mode = False 17 | global_email_language = en 18 | sqlalchemy_engine=sqlite 19 | sqlalchemy_connection_string=%(project_root)s/db/bitex.sqlite 20 | url_payment_processor = http://host_of_api_receive.com/api/receive 21 | 22 | [ws_gateway_8445_demo] 23 | port = 8445 24 | project_root=%(project_root_demo)s 25 | session_timeout_limit = 0 26 | callback_url = https://%(host_demo)s/process_deposit?s= 27 | trade_in = %(trade_in_demo)s 28 | trade_pub = %(trade_pub_demo)s 29 | db_echo = False 30 | gateway_log = %(project_root)s/logs/ws_gateway_testnet_%(port)s.log 31 | sqlalchemy_engine=sqlite 32 | sqlalchemy_connection_string=%(project_root)s/db/bitex.ws_gateway_%(port)s_testnet.sqlite 33 | url_payment_processor = http://host_of_api_receive.com/api/receive 34 | allowed_origins=["*"] 35 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | match: 5 | build: 6 | context: . 7 | dockerfile: match/Dockerfile 8 | volumes: 9 | - /opt/bitex:/opt/bitex 10 | networks: 11 | vpcbr: 12 | ipv4_address: 10.5.0.10 13 | 14 | ws_gw: 15 | build: 16 | context: . 17 | dockerfile: ws_gateway/Dockerfile 18 | volumes: 19 | - /opt/bitex:/opt/bitex 20 | ports: 21 | - 8445:8445 22 | networks: 23 | vpcbr: 24 | ipv4_address: 10.5.0.20 25 | depends_on: 26 | - match 27 | 28 | api_rcv: 29 | build: 30 | context: . 31 | dockerfile: receiver/Dockerfile 32 | volumes: 33 | - /opt/bitex:/opt/bitex 34 | ports: 35 | - 9943:9943 36 | networks: 37 | vpcbr: 38 | ipv4_address: 10.5.0.30 39 | depends_on: 40 | - bitcoind 41 | - match 42 | 43 | nginx: 44 | build: ./nginx 45 | volumes: 46 | - /opt/bitex:/opt/bitex 47 | ports: 48 | - 80:80 49 | - 443:443 50 | networks: 51 | vpcbr: 52 | ipv4_address: 10.5.0.40 53 | 54 | bitcoind: 55 | build: 56 | context: . 57 | dockerfile: bitcoind/Dockerfile 58 | depends_on: 59 | - match 60 | volumes: 61 | - /mnt/bitcoind:/bitcoin 62 | ports: 63 | - 8333:8333 64 | - 127.0.0.1:8332:8332 65 | - 18333:18333 66 | - 127.0.0.1:18332:18332 67 | 68 | environment: 69 | - TESTNET=1 70 | - DISABLEWALLET=0 71 | - PRINTTOCONSOLE=0 72 | networks: 73 | vpcbr: 74 | ipv4_address: 10.5.0.50 75 | 76 | networks: 77 | vpcbr: 78 | driver: bridge 79 | ipam: 80 | config: 81 | - subnet: 10.5.0.0/16 82 | gateway: 10.5.0.1 83 | -------------------------------------------------------------------------------- /libraries/json_encoder.py: -------------------------------------------------------------------------------- 1 | import json 2 | import datetime 3 | import decimal 4 | 5 | class JsonEncoder(json.JSONEncoder): 6 | def default(self, obj): 7 | if isinstance(obj, datetime.datetime): 8 | return obj.strftime('%Y-%m-%d %H:%M:%S') 9 | elif isinstance(obj, datetime.date): 10 | return obj.strftime('%Y-%m-%d') 11 | if isinstance(obj, datetime.time): 12 | return obj.strftime('%H:%M:%S') 13 | if isinstance(obj, decimal.Decimal): 14 | return str(obj) 15 | return json.JSONEncoder.default(self, obj) 16 | -------------------------------------------------------------------------------- /libraries/message_builder.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | 4 | class MessageBuilder(object): 5 | @staticmethod 6 | def testRequestMessage(request_id=None): 7 | if request_id: 8 | return {'MsgType': '1', 'TestReqID': request_id } 9 | else: 10 | return {'MsgType': '1', 'TestReqID': int(time.time()*1000)} 11 | 12 | @staticmethod 13 | def login(broker_id, user, password, second_factor=None): 14 | if not user or not password: 15 | raise ValueError('Invalid parameters') 16 | 17 | loginMsg = { 18 | 'UserReqID': 'initial', 19 | 'MsgType' : 'BE', 20 | 'BrokerID': broker_id, 21 | 'Username': user, 22 | 'Password': password, 23 | 'UserReqTyp': '1' 24 | } 25 | if second_factor: 26 | loginMsg['SecondFactor'] = second_factor 27 | 28 | return loginMsg 29 | 30 | @staticmethod 31 | def getDepositList(status_list, opt_filter=None, client_id=None, page=0, page_size=100,opt_request_id=None): 32 | if not opt_request_id: 33 | opt_request_id = random.randint(1,10000000) 34 | 35 | msg = { 36 | "MsgType":"U30", 37 | "DepositListReqID":opt_request_id, 38 | "Page":page, 39 | "PageSize":page_size, 40 | "StatusList":status_list 41 | } 42 | if client_id: 43 | msg["ClientID"] = client_id 44 | 45 | if opt_filter: 46 | msg["Filter"] = opt_filter 47 | return msg 48 | 49 | @staticmethod 50 | def updateProfile(update_dict, opt_user_id=None,opt_request_id=None): 51 | if not opt_request_id: 52 | opt_request_id = random.randint(1,10000000) 53 | 54 | msg = { 55 | "MsgType":"U38", 56 | "UpdateReqID":opt_request_id, 57 | "Fields": update_dict 58 | } 59 | if opt_user_id: 60 | msg["UserID"] = opt_user_id 61 | return msg 62 | 63 | @staticmethod 64 | def getWithdrawList(status_list, opt_filter=None, client_id=None, page=0, page_size=100,opt_request_id=None): 65 | if not opt_request_id: 66 | opt_request_id = random.randint(1,10000000) 67 | 68 | msg = { 69 | "MsgType":"U26", 70 | "WithdrawListReqID":opt_request_id, 71 | "Page":page, 72 | "PageSize":page_size, 73 | "StatusList":status_list, 74 | } 75 | if client_id: 76 | msg["ClientID"] = client_id 77 | if opt_filter: 78 | msg["Filter"] = opt_filter 79 | 80 | return msg 81 | 82 | @staticmethod 83 | def getBrokerList(status_list, country=None, page=None, page_size=100, opt_request_id=None): 84 | if not opt_request_id: 85 | opt_request_id = random.randint(1,10000000) 86 | 87 | msg = { 88 | 'MsgType' : 'U28', 89 | 'BrokerListReqID': opt_request_id 90 | } 91 | if page: 92 | msg['Page'] = page 93 | 94 | if page_size: 95 | msg['PageSize'] = page_size 96 | 97 | if status_list: 98 | msg['StatusList'] = status_list 99 | 100 | if country: 101 | msg['Country'] = country 102 | 103 | return msg 104 | 105 | @staticmethod 106 | def verifyCustomer(broker_id, client_id, verify, verification_data, opt_request_id=None): 107 | if not opt_request_id: 108 | opt_request_id = random.randint(1,10000000) 109 | 110 | return { 111 | 'MsgType': 'B8', 112 | 'VerifyCustomerReqID': opt_request_id, 113 | 'BrokerID': broker_id, 114 | 'ClientID': client_id, 115 | 'Verify': verify, 116 | 'VerificationData': verification_data 117 | } 118 | 119 | @staticmethod 120 | def processDeposit(action,opt_request_id = None,opt_secret=None,opt_depositId=None,opt_reasonId=None, 121 | opt_reason=None,opt_amount=None,opt_percent_fee=None,opt_fixed_fee=None): 122 | 123 | if not opt_request_id: 124 | opt_request_id = random.randint(1,10000000) 125 | 126 | msg = { 127 | 'MsgType': 'B0', 128 | 'ProcessDepositReqID': opt_request_id, 129 | 'Action': action 130 | } 131 | 132 | if opt_secret: 133 | msg['Secret'] = opt_secret 134 | 135 | if opt_depositId: 136 | msg['DepositID'] = opt_depositId 137 | 138 | if opt_reasonId: 139 | msg['ReasonID'] = opt_reasonId 140 | 141 | if opt_reason: 142 | msg['Reason'] = opt_reason 143 | 144 | if opt_amount: 145 | msg['Amount'] = opt_amount 146 | 147 | if opt_percent_fee: 148 | msg['PercentFee'] = opt_percent_fee 149 | 150 | if opt_fixed_fee: 151 | msg['FixedFee'] = opt_fixed_fee 152 | 153 | return msg 154 | 155 | @staticmethod 156 | def requestBalances(request_id = None, client_id = None): 157 | if not request_id: 158 | request_id = random.randint(1,10000000) 159 | msg = { 160 | 'MsgType': 'U2', 161 | 'BalanceReqID': request_id 162 | } 163 | if client_id: 164 | msg['ClientID'] = client_id 165 | return msg 166 | 167 | @staticmethod 168 | def requestPositions(request_id = None, client_id = None): 169 | if not request_id: 170 | request_id = random.randint(1,10000000) 171 | msg = { 172 | 'MsgType': 'U42', 173 | 'PositionReqID': request_id 174 | } 175 | if client_id: 176 | msg['ClientID'] = client_id 177 | return msg 178 | 179 | 180 | @staticmethod 181 | def requestMarketData(request_id, symbols, entry_types, subscription_type='1', market_depth=0 ,update_type = '1'): 182 | if not symbols or not entry_types: 183 | raise ValueError('Invalid parameters') 184 | 185 | return { 186 | 'MsgType' : 'V', 187 | 'MDReqID': request_id, 188 | 'SubscriptionRequestType': subscription_type, 189 | 'MarketDepth': market_depth, 190 | 'MDUpdateType': update_type, # 191 | 'MDEntryTypes': entry_types, # bid , offer, trade 192 | 'Instruments': symbols 193 | } 194 | 195 | @staticmethod 196 | def processWithdraw(action, withdrawId, request_id=None, reasonId=None, reason=None, data=None,percent_fee=None,fixed_fee=None): 197 | if not request_id: 198 | request_id = random.randint(1,10000000) 199 | 200 | msg = { 201 | 'MsgType': 'B6', 202 | 'ProcessWithdrawReqID': request_id, 203 | 'WithdrawID': withdrawId, 204 | 'Action': action 205 | } 206 | 207 | if reasonId: 208 | msg['ReasonID'] = reasonId 209 | 210 | if reason: 211 | msg['Reason'] = reason 212 | 213 | if data: 214 | msg['Data'] = data 215 | 216 | if percent_fee: 217 | msg['PercentFee'] = percent_fee 218 | 219 | if fixed_fee: 220 | msg['FixedFee'] = fixed_fee 221 | 222 | return msg 223 | 224 | @staticmethod 225 | def sendLimitedBuyOrder(symbol, qty, price, clientOrderId ): 226 | if not symbol or not qty or not qty or not price or not clientOrderId: 227 | raise ValueError('Invalid parameters') 228 | 229 | if qty <= 0 or price <= 0: 230 | raise ValueError('Invalid qty or price') 231 | 232 | return { 233 | 'MsgType': 'D', 234 | 'ClOrdID': str(clientOrderId), 235 | 'Symbol': symbol, 236 | 'Side': '1', 237 | 'OrdType': '2', 238 | 'Price': price, 239 | 'OrderQty': qty 240 | } 241 | 242 | @staticmethod 243 | def sendLimitedSellOrder(symbol, qty, price, clientOrderId ): 244 | if not symbol or not qty or not qty or not price or not clientOrderId: 245 | raise ValueError('Invalid parameters') 246 | 247 | if qty <= 0 or price <= 0: 248 | raise ValueError('Invalid qty or price') 249 | 250 | return { 251 | 'MsgType': 'D', 252 | 'ClOrdID': str(clientOrderId), 253 | 'Symbol': symbol, 254 | 'Side': '2', 255 | 'OrdType': '2', 256 | 'Price': price, 257 | 'OrderQty': qty 258 | } 259 | 260 | -------------------------------------------------------------------------------- /libraries/project_options.py: -------------------------------------------------------------------------------- 1 | class ProjectOptions(object): 2 | def __init__(self, config, section): 3 | self.config = config 4 | self.section = section 5 | 6 | def make_getters(tag): 7 | @property 8 | def _getter(self): 9 | raw_str = self.config.get(self.section, tag) 10 | try: 11 | return self.config.getint(self.section, tag) 12 | except Exception: 13 | pass 14 | try: 15 | return self.config.getfloat(self.section, tag) 16 | except Exception: 17 | pass 18 | try: 19 | return self.config.getboolean(self.section, tag) 20 | except Exception: 21 | pass 22 | 23 | return raw_str 24 | return _getter 25 | 26 | for k,v in self.items(): 27 | _getter = make_getters(k) 28 | setattr(ProjectOptions, k ,_getter) 29 | 30 | def has_option(self, attribute): 31 | return self.config.has_option(self.section, attribute) 32 | def get(self, attribute): 33 | return self.config.get(self.section, attribute) 34 | def getint(self, attribute): 35 | return self.config.getint(self.section, attribute) 36 | def getfloat(self, attribute): 37 | return self.config.getfloat(self.section, attribute) 38 | def getboolean(self, attribute): 39 | return self.config.getboolean(self.section, attribute) 40 | def items(self): 41 | return self.config.items(self.section) 42 | def options(self): 43 | return self.config.options(self.section) -------------------------------------------------------------------------------- /libraries/signals.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | import inspect 3 | import traceback 4 | import logging 5 | import threading 6 | 7 | class Signal(): 8 | signal_error = None 9 | _lock = threading.RLock() 10 | 11 | def __init__(self): 12 | self._functions = weakref.WeakSet() 13 | self._methods = weakref.WeakKeyDictionary() 14 | 15 | self._methods_subs = {} 16 | self._functions_subs = {} 17 | 18 | if not Signal.signal_error: 19 | Signal.signal_error = 1 20 | Signal.signal_error = Signal() 21 | 22 | def connect(self, slot, sender=None): 23 | if sender: 24 | if inspect.ismethod(slot): 25 | if sender not in self._methods_subs: 26 | self._methods_subs[sender] = weakref.WeakKeyDictionary() 27 | 28 | if slot.__self__ not in self._methods_subs[sender]: 29 | self._methods_subs[sender][slot.__self__] = set() 30 | 31 | self._methods_subs[sender][slot.__self__].add(slot.__func__) 32 | else: 33 | if sender not in self._functions_subs: 34 | self._functions_subs[sender] = weakref.WeakSet() 35 | self._functions_subs[sender].add(slot) 36 | else: 37 | if inspect.ismethod(slot): 38 | if slot.__self__ not in self._methods: 39 | self._methods[slot.__self__] = set() 40 | self._methods[slot.__self__].add(slot.__func__) 41 | else: 42 | self._functions.add(slot) 43 | 44 | def disconnect(self, slot, sender=None): 45 | if sender: 46 | if inspect.ismethod(slot): 47 | if sender in self._methods_subs: 48 | if slot.__self__ in self._methods_subs[sender]: 49 | self._methods_subs[sender][slot.__self__].remove(slot.__func__) 50 | if len(self._methods_subs[sender][slot.__self__])== 0: 51 | del self._methods_subs[sender][slot.__self__] 52 | if len(self._methods_subs[sender]) ==0: 53 | del self._methods_subs[sender] 54 | else: 55 | if sender in self._functions_subs: 56 | self._functions_subs[sender].remove(slot) 57 | if len(self._functions_subs[sender]) == 0: 58 | del self._functions_subs[sender] 59 | else: 60 | if inspect.ismethod(slot): 61 | if slot.__self__ in self._methods: 62 | del self._methods[slot.__self__][slot.__func__] 63 | else: 64 | self._functions.remove(slot) 65 | 66 | def __call__(self, sender, data=None, error_signal_on_error=True): 67 | with self._lock: 68 | sent = False 69 | errors = [] 70 | 71 | def publish_functions(functions): 72 | for func in functions: 73 | try: 74 | func(sender, data) 75 | sent = True 76 | 77 | # pylint: disable=W0702 78 | except: 79 | errors.append(traceback.format_exc()) 80 | publish_functions(self._functions) 81 | if sender in self._functions_subs: 82 | publish_functions(self._functions_subs[sender]) 83 | if not self._functions_subs[sender]: 84 | del self._functions_subs[sender] 85 | 86 | 87 | 88 | def publish_methods( methods ): 89 | for obj, funcs in methods.items(): 90 | for func in funcs: 91 | try: 92 | func(obj, sender, data) 93 | sent = True 94 | 95 | # pylint: disable=W0702 96 | except: 97 | errors.append(traceback.format_exc()) 98 | publish_methods(self._methods) 99 | 100 | if sender in self._methods_subs: 101 | publish_methods(self._methods_subs[sender]) 102 | if not self._methods_subs[sender]: 103 | del self._methods_subs[sender] 104 | 105 | 106 | for error in errors: 107 | if error_signal_on_error: 108 | Signal.signal_error(self, (error), False) 109 | else: 110 | logging.critical(error) 111 | 112 | return sent 113 | 114 | -------------------------------------------------------------------------------- /libraries/utils.py: -------------------------------------------------------------------------------- 1 | import types 2 | 3 | 4 | def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'): 5 | """ 6 | Returns a bytestring version of 's', encoded as specified in 'encoding'. 7 | 8 | If strings_only is True, don't convert (some) non-string-like objects. 9 | """ 10 | if strings_only and isinstance(s, (types.NoneType, int)): 11 | return s 12 | if not isinstance(s, basestring): 13 | try: 14 | return str(s) 15 | except UnicodeEncodeError: 16 | if isinstance(s, Exception): 17 | # An Exception subclass containing non-ASCII data that doesn't 18 | # know how to print itself properly. We shouldn't raise a 19 | # further exception. 20 | return ' '.join([smart_str(arg, encoding, strings_only, 21 | errors) for arg in s]) 22 | return unicode(s).encode(encoding, errors) 23 | elif isinstance(s, unicode): 24 | return s.encode(encoding, errors) 25 | elif s and encoding != 'utf-8': 26 | return s.decode('utf-8', errors).encode(encoding, errors) 27 | else: 28 | return s 29 | -------------------------------------------------------------------------------- /libraries/zmq_client.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | 4 | import zmq 5 | from zmq.eventloop.zmqstream import ZMQStream 6 | 7 | from message_builder import MessageBuilder 8 | from message import JsonMessage, InvalidMessageException 9 | 10 | class TradeClientException(Exception): 11 | def __init__(self, error_message, detail = None): 12 | self.error_message = error_message 13 | self.detail = None 14 | super(TradeClientException, self).__init__() 15 | 16 | def __str__(self): 17 | if self.detail: 18 | return self.error_message + '( detail:%s)'%self.detail 19 | return self.error_message 20 | 21 | 22 | 23 | class TradeClient(object): 24 | def __init__(self, zmq_context, trade_in_socket, trade_pub = None, reopen=True): 25 | self.zmq_context = zmq_context 26 | self.connection_id = None 27 | self.trade_in_socket = trade_in_socket 28 | self.is_logged = False 29 | self.user_id = None 30 | self.reopen = reopen 31 | self.trade_pub = trade_pub 32 | 33 | self.trade_pub_socket = None 34 | self.trade_pub_socket_stream = None 35 | 36 | if self.trade_pub: 37 | self.trade_pub_socket = self.zmq_context.socket(zmq.SUB) 38 | self.trade_pub_socket.connect(self.trade_pub) 39 | self.trade_pub_socket_stream = ZMQStream(self.trade_pub_socket) 40 | self.trade_pub_socket_stream.on_recv(self._on_trade_publish) 41 | 42 | def _on_trade_publish(self, message): 43 | self.on_trade_publish(message) 44 | 45 | def on_trade_publish(self, message): 46 | pass 47 | 48 | def connect(self): 49 | if not self.trade_pub_socket and self.trade_pub: 50 | self.trade_pub_socket = self.zmq_context.socket(zmq.SUB) 51 | self.trade_pub_socket.connect(self.trade_pub) 52 | self.trade_pub_socket_stream = ZMQStream(self.trade_pub_socket) 53 | self.trade_pub_socket_stream.on_recv(self._on_trade_publish) 54 | 55 | self.trade_in_socket.send( "OPN," + base64.b32encode(os.urandom(10))) 56 | response_message = self.trade_in_socket.recv() 57 | opt_code = response_message[:3] 58 | raw_message = response_message[4:] 59 | 60 | if opt_code != 'OPN': 61 | if opt_code == 'ERR': 62 | raise TradeClientException( error_message = raw_message ) 63 | 64 | raise TradeClientException( error_message = 'Protocol Error: Unknown message opt_code received' ) 65 | 66 | self.connection_id = raw_message 67 | 68 | 69 | def close(self): 70 | if self.trade_pub_socket_stream: 71 | self.trade_pub_socket_stream.close() 72 | self.trade_pub_socket_stream = None 73 | self.trade_pub_socket.close() 74 | self.trade_pub_socket = None 75 | 76 | if self.connection_id: 77 | self.trade_in_socket.send( "CLS," + self.connection_id ) 78 | response_message = self.trade_in_socket.recv() 79 | self.connection_id = None 80 | 81 | def isConnected(self): 82 | return self.connection_id is not None 83 | 84 | def getBrokerList(self, status_list): 85 | page = 0 86 | page_size = 100 87 | 88 | broker_list = [] 89 | columns = [] 90 | while True: 91 | res = self.sendJSON(MessageBuilder.getBrokerList( status_list, None, page, page_size )) 92 | columns = res.get('Columns') 93 | broker_list.extend(res.get('BrokerListGrp')) 94 | if len(res.get('BrokerListGrp')) < page_size: 95 | break 96 | page += 1 97 | 98 | return broker_list, columns 99 | 100 | def getLastTrades(self, last_trade_id, page=0): 101 | 102 | if last_trade_id is None: 103 | last_trade_id = 1 104 | 105 | rep_msg = self.sendJSON({ 'MsgType': 'U32', 106 | 'TradeHistoryReqID': -1, 107 | 'Page': page, 108 | 'PageSize': 100 }) 109 | 110 | result_list = [] 111 | 112 | if rep_msg.isTradeHistoryResponse(): 113 | last_processed = 0 114 | trade_list = rep_msg.get('TradeHistoryGrp') 115 | if not trade_list : 116 | return result_list 117 | for trade in trade_list: 118 | if last_trade_id <= trade[0]: 119 | last_processed = trade[0] 120 | if last_processed != last_trade_id: 121 | result_list.append(trade) 122 | 123 | 124 | if last_processed != last_trade_id: 125 | return result_list + self.getLastTrades(last_trade_id, page+1) 126 | 127 | return result_list 128 | 129 | 130 | def getSecurityList(self): 131 | resp = self.sendJSON({ 'MsgType' : 'x', 132 | 'SecurityReqID': 'getSecurityList', 133 | 'SecurityListRequestType': 0}) 134 | return resp 135 | 136 | 137 | 138 | def sendString(self, string_msg): 139 | if not self.isConnected() and self.reopen: 140 | self.connect() 141 | 142 | self.trade_in_socket.send_unicode( "REQ," + self.connection_id + ',' + string_msg) 143 | 144 | response_message = self.trade_in_socket.recv() 145 | raw_resp_message_header = response_message[:3] 146 | raw_resp_message = response_message[4:].strip() 147 | 148 | rep_msg = None 149 | if raw_resp_message: 150 | try: 151 | rep_msg = JsonMessage(raw_resp_message) 152 | except Exception: 153 | pass 154 | 155 | if raw_resp_message_header == 'CLS' and rep_msg and not rep_msg.isErrorMessage(): 156 | self.close() 157 | if self.reopen: 158 | self.connect() 159 | return rep_msg 160 | 161 | if raw_resp_message_header != 'REP': 162 | self.close() 163 | if self.reopen: 164 | self.connect() 165 | 166 | if rep_msg and rep_msg.isErrorMessage(): 167 | raise TradeClientException(rep_msg.get('Description'), rep_msg.get('Detail')) 168 | raise TradeClientException('Invalid request: ' + raw_resp_message ) 169 | 170 | 171 | if rep_msg and rep_msg.isUserResponse(): 172 | if rep_msg.get("UserStatus") == 1: 173 | self.user_id = rep_msg.get("UserID") 174 | self.is_logged = True 175 | 176 | if self.trade_pub_socket: 177 | self.trade_pub_socket.setsockopt(zmq.SUBSCRIBE, '^' + str(self.user_id) + '$') 178 | 179 | return rep_msg 180 | 181 | def sendJSON(self, json_msg): 182 | import json 183 | return self.sendString(json.dumps(json_msg)) 184 | 185 | def sendMessage(self, msg): 186 | return self.sendString(msg.raw_message) 187 | -------------------------------------------------------------------------------- /mailer/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'rodrigo' 2 | -------------------------------------------------------------------------------- /mailer/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | import os 5 | import traceback 6 | import argparse 7 | import ConfigParser 8 | from appdirs import site_config_dir 9 | 10 | ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")) 11 | sys.path.insert(0, os.path.join( os.path.dirname(__file__), '../' ) ) 12 | 13 | import time 14 | from util import send_email 15 | 16 | from pyblinktrade.message_builder import MessageBuilder 17 | from pyblinktrade.message import JsonMessage 18 | from pyblinktrade.project_options import ProjectOptions 19 | 20 | import logging, logging.handlers 21 | 22 | import json 23 | import zmq 24 | 25 | import mailchimp 26 | import mandrill 27 | 28 | from trade.zmq_client import TradeClient, TradeClientException 29 | 30 | def convertCamelCase2Underscore(name): 31 | import re 32 | s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) 33 | return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() 34 | 35 | 36 | def run_application(options, instance_name): 37 | input_log_file_handler = logging.handlers.TimedRotatingFileHandler( 38 | os.path.expanduser(options.mailer_log), when='MIDNIGHT') 39 | input_log_file_handler.setFormatter(logging.Formatter(u"%(asctime)s - %(message)s")) 40 | 41 | mail_logger = logging.getLogger(instance_name) 42 | mail_logger.setLevel(logging.INFO) 43 | mail_logger.addHandler(input_log_file_handler) 44 | 45 | ch = logging.StreamHandler(sys.stdout) 46 | ch.setLevel(logging.DEBUG) 47 | ch.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 48 | mail_logger.addHandler(ch) 49 | 50 | mail_logger.info('START') 51 | def log(command, key, value=None): 52 | log_msg = u'%s, %s, %s' %(command, key, value if value else None) 53 | mail_logger.info(unicode(log_msg)) 54 | 55 | 56 | log('PARAM', 'BEGIN') 57 | log('PARAM', 'trade_in', options.trade_in) 58 | log('PARAM', 'trade_pub', options.trade_pub) 59 | log('PARAM', 'mailer_log', options.mailer_log) 60 | log('PARAM', 'mailchimp_apikey', options.mailchimp_apikey) 61 | log('PARAM', 'mandrill_apikey', options.mandrill_apikey) 62 | log('PARAM', 'mailer_username', options.mailer_username) 63 | log('PARAM', 'mailchimp_newsletter_list_id', options.mailchimp_newsletter_list_id) 64 | log('PARAM', 'END') 65 | 66 | context = zmq.Context() 67 | socket = context.socket(zmq.SUB) 68 | socket.connect(options.trade_pub) 69 | socket.setsockopt(zmq.SUBSCRIBE, "^EMAIL$") 70 | 71 | trade_in_socket = context.socket(zmq.REQ) 72 | trade_in_socket.connect(options.trade_in) 73 | 74 | application_trade_client = TradeClient( context, trade_in_socket) 75 | application_trade_client.connect() 76 | 77 | login_response = application_trade_client.sendJSON( MessageBuilder.login( 8999999, 78 | options.mailer_username, 79 | options.mailer_password)) 80 | if login_response.get('UserStatus') != 1: 81 | raise RuntimeError("Invalid user id") 82 | 83 | brokers = {} 84 | broker_list, broker_list_columns = application_trade_client.getBrokerList(['1']) 85 | for b in broker_list: 86 | brokers[b[0]] = { "params": b } 87 | 88 | broker_mandrill_column_index = None 89 | try: 90 | broker_mandrill_column_index = broker_list_columns.index('MandrillApiKey') 91 | except ValueError: 92 | pass 93 | 94 | 95 | for broker_id, broker_data in brokers.iteritems(): 96 | if broker_mandrill_column_index and broker_data['params'][ broker_mandrill_column_index ]: 97 | broker_data['MandrillApiKey'] = broker_data['params'][ broker_mandrill_column_index ] 98 | else: 99 | broker_data['MandrillApiKey'] = options.mandrill_apikey 100 | 101 | for broker_id, broker_data in brokers.iteritems(): 102 | print broker_id, broker_data['MandrillApiKey'] 103 | 104 | # [u'BrokerID', u'ShortName', u'BusinessName', u'Address', u'City', u'State', 105 | # u'ZipCode', u'Country', u'PhoneNumber1', u'PhoneNumber2', u'Skype', u'Currencies', 106 | # u'TosUrl', u'FeeStructure', u'TransactionFeeBuy', u'TransactionFeeSell', u'Status', 107 | # u'ranking', u'Email', u'CountryCode', u'CryptoCurrencies', u'WithdrawStructure', 108 | # u'SupportURL', u'SignupLabel', u'AcceptCustomersFrom', u'IsBrokerHub'] 109 | 110 | mailchimp_api = mailchimp.Mailchimp(options.mailchimp_apikey) 111 | try: 112 | mailchimp_api.helper.ping() 113 | except mailchimp.Error: 114 | raise RuntimeError("Invalid MailChimp API key") 115 | 116 | mandrill_api = mandrill.Mandrill(options.mandrill_apikey) 117 | try: 118 | mandrill_api.users.ping() 119 | except mandrill.Error: 120 | raise RuntimeError("Invalid Mandrill API key") 121 | 122 | while True: 123 | try: 124 | raw_email_message = socket.recv() 125 | log('IN', 'TRADE_IN_PUB', raw_email_message) 126 | 127 | msg = JsonMessage(raw_email_message) 128 | 129 | if not msg.isEmail(): 130 | log('ERROR', 131 | 'EXCEPTION', 132 | 'Received message is not an email message') 133 | continue 134 | 135 | try: 136 | broker_id = msg.get('BrokerID') 137 | sender = brokers[broker_id]['params'][broker_list_columns.index('MailerFromName')] + \ 138 | '<' + brokers[broker_id]['params'][broker_list_columns.index('MailerFromEmail')] + '>' 139 | body = "" 140 | msg_to = msg.get('To') 141 | subject = msg.get('Subject') 142 | language = msg.get('Language') 143 | content_type = 'plain' 144 | 145 | if msg.has('Template') and msg.get('Template'): 146 | params = {} 147 | if msg.has('Params') and msg.get('Params'): 148 | params = json.loads(msg.get('Params')) 149 | 150 | template_name = msg.get('Template') 151 | 152 | 153 | if template_name == 'welcome': 154 | # user signup .... let's register him on mailchimp newsletter 155 | try: 156 | mailchimp_api.lists.subscribe( 157 | id = brokers[broker_id]['params'][broker_list_columns.index('MailchimpListID')], 158 | email = {'email': params['email'] }, 159 | merge_vars = {'EMAIL' : params['email'], 'FNAME': params['username'] } ) 160 | 161 | except mailchimp.ListAlreadySubscribedError: 162 | log('ERROR', 'EXCEPTION', params['email'] + ' mailchimp.ListAlreadySubscribedError' ) 163 | except mailchimp.Error, e: 164 | log('ERROR', 'EXCEPTION', str(e)) 165 | 166 | 167 | template_content = [] 168 | for k,v in params.iteritems(): 169 | template_content.append( { 'name': k, 'content': v } ) 170 | 171 | for broker_column_key in broker_list_columns: 172 | broker_column_value = brokers[broker_id]['params'][broker_list_columns.index(broker_column_key)] 173 | template_content.append( { 'name': 'broker_' + convertCamelCase2Underscore(broker_column_key), 174 | 'content': broker_column_value } ) 175 | 176 | 177 | message = { 178 | 'from_email': brokers[broker_id]['params'][broker_list_columns.index('MailerFromEmail')], 179 | 'from_name': brokers[broker_id]['params'][broker_list_columns.index('MailerFromName')], 180 | 'to': [{'email': msg_to, 'name': params['username'],'type': 'to' }], 181 | 'metadata': {'website': 'www.blinktrade.com'}, 182 | 'global_merge_vars': template_content 183 | } 184 | 185 | result = mandrill_api.messages.send_template( 186 | template_name= (template_name + '-' + language.replace('_','-') ).lower(), 187 | template_content=template_content, 188 | message=message) 189 | 190 | log('INFO', 'SUCCESS', str(result)) 191 | continue 192 | 193 | elif msg.has('RawData') and msg.get('RawData'): 194 | body = msg.get('RawData') 195 | 196 | log('DEBUG', 'EMAIL', 197 | u'{"Sender":"%s","To":"%s","Subject":"%s", "Body":"%s" }' % (sender, 198 | msg_to, 199 | subject, 200 | body)) 201 | send_email(sender, msg_to, subject, body, content_type) 202 | log('IN', 'SENT', "") 203 | 204 | log('INFO', 'SUCCESS', msg.get('EmailThreadID')) 205 | except Exception as ex: 206 | traceback.print_exc() 207 | log('ERROR', 'EXCEPTION', str(ex)) 208 | time.sleep(1) 209 | 210 | except KeyboardInterrupt: 211 | mail_logger.info('END') 212 | break 213 | 214 | except Exception as ex: 215 | time.sleep(1) 216 | 217 | 218 | def main(): 219 | parser = argparse.ArgumentParser(description="Blinktrade Mailer application") 220 | parser.add_argument('-i', "--instance", action="store", dest="instance", help='Instance name', type=str) 221 | parser.add_argument('-c', "--config", 222 | action="store", 223 | dest="config", 224 | default=os.path.expanduser('~/.blinktrade/bitex.ini'), 225 | help='Configuration file', type=str) 226 | arguments = parser.parse_args() 227 | 228 | if not arguments.instance: 229 | parser.print_help() 230 | return 231 | 232 | candidates = [ os.path.join(site_config_dir('blinktrade'), 'bitex.ini'), 233 | os.path.expanduser('~/.blinktrade/bitex.ini'), 234 | arguments.config] 235 | config = ConfigParser.SafeConfigParser() 236 | config.read( candidates ) 237 | 238 | options = ProjectOptions(config, arguments.instance) 239 | 240 | if not options.mailchimp_apikey or\ 241 | not options.mandrill_apikey or\ 242 | not options.mailchimp_newsletter_list_id or\ 243 | not options.trade_in or\ 244 | not options.mailer_username or\ 245 | not options.mailer_password or\ 246 | not options.trade_pub or \ 247 | not options.mailer_log : 248 | raise RuntimeError("Invalid configuration file") 249 | 250 | run_application(options, arguments.instance) 251 | 252 | 253 | if __name__ == '__main__': 254 | main() 255 | -------------------------------------------------------------------------------- /mailer/templates/customer-verification-submit-en.txt: -------------------------------------------------------------------------------- 1 | template-name=customer-verification-submit-en 2 | template-slug=customer-verification-submit-en 3 | temaplate-defaults-from-address=support@blinktrade.zendesk.com 4 | template-defaults-from-name=BlinkTrade 5 | template-defaults-subject=Your account has been sent for verification 6 | 7 | Hi *|username|*,
8 |
9 | Your data has been sent, we will validate your account shortly. 10 | .
11 |
12 | Thank you
13 | BlinkTrade 14 | -------------------------------------------------------------------------------- /mailer/templates/order-execution-en.txt: -------------------------------------------------------------------------------- 1 | template-name=order-execution-en 2 | template-slug=order-execution-en 3 | temaplate-defaults-from-address=support@blinktrade.zendesk.com 4 | template-defaults-from-name=BlinkTrade 5 | template-defaults-subject=Your order *|order_id|* was executed. 6 | 7 | Hi *|username|*,
8 |
9 | Your order was executed.
10 |
11 | Number: *|order_id|*
12 | Ejecuciono Number: *|trade_id|*
13 | Command executed in: *|executed_when|* (UTC)
14 | Quantity: *|qty|*
15 | Price: *|price|*
16 | Total: *|total|*
17 |
18 | Thank you
19 | BlinkTrade
-------------------------------------------------------------------------------- /mailer/templates/password-reset-en.txt: -------------------------------------------------------------------------------- 1 | template-name=password-reset-en 2 | template-slug=password-reset-en 3 | temaplate-defaults-from-address=support@blinktrade.zendesk.com 4 | template-defaults-from-name=BlinkTrade 5 | template-defaults-subject=Instructions to reset your password 6 | 7 | Hi *|username|*
8 |
9 | Enter the following security code to reset your password. *|Token|*
10 |
11 | Thank you
12 | BlinkTrade
-------------------------------------------------------------------------------- /mailer/templates/welcome-en.txt: -------------------------------------------------------------------------------- 1 | template-name=welcome-en 2 | template-slug=welcome-en 3 | temaplate-defaults-from-address=support@blinktrade.zendesk.com 4 | template-defaults-from-name=BlinkTrade 5 | template-defaults-subject=Hi *|USERNAME|*, Welcome to BlinkTrade 6 | 7 | Hi *|USERNAME|*
8 |  
9 | Welcome to BlinkTrade
10 |  
11 | Thank you
12 | BlinkTrade
-------------------------------------------------------------------------------- /mailer/templates/withdraw-cancelled-en.txt: -------------------------------------------------------------------------------- 1 | template-name=withdraw-cancelled-en 2 | template-slug=withdraw-cancelled-en 3 | temaplate-defaults-from-address=support@blinktrade.zendesk.com 4 | template-defaults-from-name=BlinkTrade 5 | template-defaults-subject=Your withdrawal request *|amount|* *|currency|* was canceled 6 | 7 | Hi *|username|*
8 |
9 | Your withdrawal request made ​​on *|created|* UTC to the value of *|amount|* *|currency|* was canceled for the following reason:
10 |
11 | Error code: *|reason_id|*
12 | Description: *|reason|*
13 |
14 | Thank you
15 | BlinkTrade -------------------------------------------------------------------------------- /mailer/templates/withdraw-confirmation-bank-transfer-en.txt: -------------------------------------------------------------------------------- 1 | template-name=withdraw-confirmation-bank-transfer-en 2 | template-slug=withdraw-confirmation-bank-transfer-en 3 | temaplate-defaults-from-address=support@blinktrade.zendesk.com 4 | template-defaults-from-name=BlinkTrade 5 | template-defaults-subject=Confirm the withdraw order *|currency|* 6 | 7 | Hi *|username|*
8 |
9 | This is your confirmation code
10 | *|confirmation_token|*
11 |
12 | Attention this confirmation code is valid to confirm your withdraw order
13 | Facts: *|created|*
UTC 14 | Value: *|amount|*
15 | Currency: *|currency|*
16 | Destination: *|wallet|*
17 |
18 | Attention:
19 | This procedure is necessary to increase the security of the system, because if your BlinkTrade password is compromised, the attacker does not make the order Put Stone retirement bitcoins without access to their email account.
20 |
21 | Thank you
-------------------------------------------------------------------------------- /mailer/templates/withdraw-confirmation-bitcoin-en.txt: -------------------------------------------------------------------------------- 1 | template-name=withdraw-confirmation-bitcoin-en 2 | template-slug=withdraw-confirmation-bitcoin-en 3 | temaplate-defaults-from-address=support@blinktrade.zendesk.com 4 | template-defaults-from-name=BlinkTrade 5 | template-defaults-subject=Confirm the withdraw order *|currency|* 6 | 7 | Hi *|username|*
8 |
9 | This is your confirmation code
10 | *|confirmation_token|*
11 |
12 | Attention this confirmation code is valid to confirm your withdraw order
13 | Facts: *|created|*
UTC 14 | Value: *|amount|*
15 | Currency: *|currency|*
16 | Destination: *|wallet|*
17 |
18 | Attention:
19 | This procedure is necessary to increase the security of the system, because if your BlinkTrade password is compromised, the attacker does not make the order Put Stone retirement bitcoins without access to their email account.
20 |
21 | Thank you
-------------------------------------------------------------------------------- /mailer/templates/your-account-has-been-verified-en.txt: -------------------------------------------------------------------------------- 1 | template-name=your-account-has-been-verified-en 2 | template-slug=your-account-has-been-verified-en 3 | temaplate-defaults-from-address=support@blinktrade.zendesk.com 4 | template-defaults-from-name=BlinkTrade 5 | template-defaults-subject=Your account has been verified 6 | 7 | Hi *|username|*,
8 |
9 | Your account has been verified.
10 |
11 | Thank you,
12 | BlinkTrade -------------------------------------------------------------------------------- /mailer/util.py: -------------------------------------------------------------------------------- 1 | from smtplib import SMTP 2 | from email.MIMEText import MIMEText 3 | from email.Header import Header 4 | from email.Utils import parseaddr, formataddr 5 | 6 | 7 | def send_email(sender, recipient, subject, body, content_type='plain'): 8 | """Send an email. 9 | 10 | All arguments should be Unicode strings (plain ASCII works as well). 11 | 12 | Only the real name part of sender and recipient addresses may contain 13 | non-ASCII characters. 14 | 15 | The email will be properly MIME encoded and delivered though SMTP to 16 | localhost port 25. This is easy to change if you want something different. 17 | 18 | The charset of the email will be the first one out of US-ASCII, ISO-8859-1 19 | and UTF-8 that can represent all the characters occurring in the email. 20 | """ 21 | 22 | # Header class is smart enough to try US-ASCII, then the charset we 23 | # provide, then fall back to UTF-8. 24 | header_charset = 'ISO-8859-1' 25 | 26 | # We must choose the body charset manually 27 | for body_charset in 'US-ASCII', 'ISO-8859-1', 'UTF-8': 28 | try: 29 | body.encode(body_charset) 30 | except UnicodeError: 31 | pass 32 | else: 33 | break 34 | 35 | # Split real name (which is optional) and email address parts 36 | sender_name, sender_addr = parseaddr(sender) 37 | recipient_name, recipient_addr = parseaddr(recipient) 38 | 39 | # We must always pass Unicode strings to Header, otherwise it will 40 | # use RFC 2047 encoding even on plain ASCII strings. 41 | sender_name = str(Header(unicode(sender_name), header_charset)) 42 | recipient_name = str(Header(unicode(recipient_name), header_charset)) 43 | 44 | # Make sure email addresses do not contain non-ASCII characters 45 | sender_addr = sender_addr.encode('ascii') 46 | recipient_addr = recipient_addr.encode('ascii') 47 | 48 | # Create the message ('plain' stands for Content-Type: text/plain) 49 | msg = MIMEText(body.encode(body_charset), content_type, body_charset) 50 | msg['From'] = formataddr((sender_name, sender_addr)) 51 | msg['To'] = formataddr((recipient_name, recipient_addr)) 52 | msg['Subject'] = Header(unicode(subject), header_charset) 53 | 54 | try: 55 | # Send the message via SMTP to localhost:25 56 | smtp = SMTP("127.0.0.1") 57 | smtp.ehlo() 58 | smtp.sendmail(sender_addr, recipient, msg.as_string()) 59 | smtp.quit() 60 | except Exception as ex: 61 | pass 62 | -------------------------------------------------------------------------------- /match/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7 2 | ADD requirements.txt /tmp/requirements.txt 3 | RUN pip install -r /tmp/requirements.txt 4 | ADD match /code 5 | ADD bitex.ini /code 6 | ADD libraries /libraries 7 | WORKDIR /code 8 | RUN mkdir -p /opt/bitex/db && mkdir -p /opt/bitex/logs 9 | ENTRYPOINT ["python"] 10 | CMD [ "main.py", "-i", "trade_demo" ] 11 | -------------------------------------------------------------------------------- /match/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /match/decorators.py: -------------------------------------------------------------------------------- 1 | from errors import * 2 | 3 | def login_required(func): 4 | def decorator(session,*args, **kwargs): 5 | if session.user is None : 6 | raise NotAuthorizedError() 7 | return func(session, *args, **kwargs) 8 | return decorator 9 | 10 | def staff_user_required(func): 11 | def decorator(session,*args, **kwargs): 12 | if session.user is None or session.user.is_staff == False: 13 | raise NotAuthorizedError() 14 | return func(session, *args, **kwargs) 15 | return decorator 16 | 17 | def broker_user_required(func): 18 | def decorator(session,*args, **kwargs): 19 | if session.user is None or session.user.is_broker == False: 20 | raise NotAuthorizedError() 21 | return func(session, *args, **kwargs) 22 | return decorator 23 | 24 | def system_user_required(func): 25 | def decorator(session,*args, **kwargs): 26 | if session.user is None or session.user.is_system == False: 27 | raise NotAuthorizedError() 28 | return func(session, *args, **kwargs) 29 | return decorator 30 | 31 | def verified_user_required(func): 32 | def decorator(session,*args, **kwargs): 33 | if session.user is None or session.user.verified < 2: 34 | raise NotAuthorizedError() 35 | return func(session, *args, **kwargs) 36 | return decorator 37 | 38 | 39 | 40 | def verify_permission(func): 41 | def decorator(session, msg): 42 | if '*' in session.permission_list: 43 | return func(session, msg) 44 | 45 | if msg.type not in session.permission_list: 46 | raise NotAuthorizedError() 47 | 48 | 49 | msg_permission_filter_list = session.permission_list[msg.type] 50 | if not msg_permission_filter_list: 51 | return func(session, msg) 52 | 53 | def pass_filter(msg, field, operator, value): 54 | if operator == 'eq': 55 | if not msg.has(field): 56 | return False 57 | if msg.get(field) != value: 58 | return False 59 | elif operator == 'in': 60 | if not msg.has(field): 61 | return False 62 | if msg.get(field) not in value: 63 | return False 64 | elif operator == 'ne': 65 | if msg.has(field): 66 | if msg.get(field) == value: 67 | return False 68 | elif operator == 'gt': 69 | if not msg.has(field): 70 | return False 71 | if msg.get(field) <= value: 72 | return False 73 | elif operator == 'ge': 74 | if not msg.has(field): 75 | return False 76 | if msg.get(field) < value: 77 | return False 78 | elif operator == 'lt': 79 | if not msg.has(field): 80 | return False 81 | if msg.get(field) >= value: 82 | return False 83 | elif operator == 'le': 84 | if not msg.has(field): 85 | return False 86 | if msg.get(field) > value: 87 | return False 88 | return True 89 | 90 | 91 | passed_one_of_the_filters = False 92 | for permission_filter in msg_permission_filter_list: 93 | passed_on_all_the_filters = True 94 | for x in xrange(0, len(permission_filter), 3): 95 | field = permission_filter[x+0] 96 | operator = permission_filter[x+1] 97 | value = permission_filter[x+2] 98 | 99 | if not pass_filter(msg, field, operator, value): 100 | passed_on_all_the_filters = False 101 | break 102 | 103 | if not passed_on_all_the_filters: 104 | continue 105 | 106 | passed_one_of_the_filters = True 107 | break 108 | 109 | if not passed_one_of_the_filters: 110 | raise NotAuthorizedError 111 | 112 | return func(session, msg) 113 | return decorator 114 | -------------------------------------------------------------------------------- /match/errors.py: -------------------------------------------------------------------------------- 1 | __author__ = 'rodrigo' 2 | 3 | 4 | 5 | class TradeRuntimeError(RuntimeError): 6 | error_description = "Unknow error" 7 | 8 | class DuplicateSession(TradeRuntimeError): 9 | error_description = "Duplicated session" 10 | 11 | class UserAlreadyLogged(TradeRuntimeError): 12 | error_description = "User is already logged in" 13 | 14 | class InvalidOptCodeError(TradeRuntimeError): 15 | error_description = "Invalid message opt_code" 16 | 17 | class InvalidSessionError(TradeRuntimeError): 18 | error_description = "Invalid session" 19 | 20 | class SessionTimeoutError(TradeRuntimeError): 21 | error_description = "Session timeout" 22 | 23 | class InvalidMessageError(TradeRuntimeError): 24 | error_description = "Invalid message" 25 | 26 | class NotAuthorizedError(TradeRuntimeError): 27 | error_description = "Not authorized" 28 | 29 | class InvalidClientIDError(TradeRuntimeError): 30 | error_description = "Invalid Client ID" 31 | 32 | class InvalidParameter(TradeRuntimeError): 33 | error_description = "Invalid Parameter" 34 | 35 | class InvalidApiKeyError(TradeRuntimeError): 36 | error_description = "ApiKey is not valid" 37 | 38 | class ApiKeyIsNotRevocableError(TradeRuntimeError): 39 | error_description = "ApiKey is not revocable" 40 | -------------------------------------------------------------------------------- /match/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | import ConfigParser 5 | import argparse 6 | from appdirs import site_config_dir 7 | 8 | sys.path.insert(0, os.path.join( os.path.dirname(__file__), '../libraries/' ) ) 9 | 10 | from project_options import ProjectOptions 11 | from trade_application import TradeApplication 12 | 13 | def main(): 14 | parser = argparse.ArgumentParser(description="Bitex Trade application") 15 | parser.add_argument('-i', "--instance", 16 | action="store", 17 | dest="instance", 18 | help='Instance name', 19 | type=str) 20 | parser.add_argument('-c', "--config", 21 | action="store", 22 | dest="config", 23 | default=os.path.expanduser('~/.bitex/bitex.ini'), 24 | help='Configuration file', type=str) 25 | arguments = parser.parse_args() 26 | 27 | if not arguments.instance: 28 | parser.print_help() 29 | return 30 | 31 | config_file = "/code/bitex.ini" 32 | if not os.path.exists( config_file ): 33 | raise RuntimeError("Configuration file not found") 34 | 35 | config = ConfigParser.SafeConfigParser() 36 | config.read( config_file ) 37 | 38 | options = ProjectOptions(config, arguments.instance) 39 | 40 | if not options.trade_in or \ 41 | not options.trade_pub or \ 42 | not options.trade_log or \ 43 | not options.global_email_language or \ 44 | not options.sqlalchemy_connection_string or \ 45 | not options.sqlalchemy_engine: 46 | raise RuntimeError("Invalid configuration file") 47 | 48 | application = TradeApplication.instance() 49 | application.initialize(options, arguments.instance) 50 | application.run() 51 | 52 | if __name__ == "__main__": 53 | main() 54 | -------------------------------------------------------------------------------- /match/market_data_publisher.py: -------------------------------------------------------------------------------- 1 | __author__ = 'rodrigo' 2 | 3 | from models import Trade 4 | from trade_application import TradeApplication 5 | 6 | class MarketDataPublisher(object): 7 | def __init__(self, market_depth, entry, instrument, handler): 8 | self.handler = handler 9 | 10 | @staticmethod 11 | def publish_executions(symbol, entry_type, executed_count, order = None): 12 | entry_list = [] 13 | 14 | if executed_count: 15 | entry_list.append( { 16 | "MDUpdateAction":"3", # Delete Thru 17 | "Symbol": symbol, 18 | "MDEntryType": entry_type, 19 | "MDEntryPositionNo": executed_count, 20 | }) 21 | 22 | if order: 23 | entry_list.append( { 24 | "MDUpdateAction":"1", # Update 25 | "Symbol": symbol, 26 | "MDEntryType": entry_type, 27 | "MDEntryPositionNo": 1, 28 | "MDEntryID": order.id, 29 | "MDEntryPx": order.price, 30 | "MDEntrySize": order.leaves_qty, 31 | "MDEntryDate": order.created.date(), 32 | "MDEntryTime": order.created.time(), 33 | "OrderID": order.id, 34 | "UserID": order.account_id, 35 | "Username": order.account_username, 36 | "Broker": order.broker_username 37 | }) 38 | if entry_list: 39 | md = { 40 | "MsgType":"X", 41 | "MDBkTyp": '3', # Order Depth 42 | "MDIncGrp": entry_list 43 | } 44 | TradeApplication.instance().publish( 'MD_INCREMENTAL_' + symbol + '.' + entry_type , md ) 45 | 46 | @staticmethod 47 | def publish_cancel_order(symbol, entry_type, order_position ): 48 | 49 | md = { 50 | "MsgType":"X", 51 | "MDBkTyp": '3', # Order Depth 52 | "MDIncGrp": [{ 53 | "MDUpdateAction":"2", # Delete 54 | "Symbol": symbol, 55 | "MDEntryType": entry_type, 56 | "MDEntryPositionNo": order_position, 57 | }] 58 | } 59 | TradeApplication.instance().publish( 'MD_INCREMENTAL_' + symbol + '.' + entry_type , md ) 60 | 61 | 62 | @staticmethod 63 | def publish_new_order(symbol, entry_type, order_position, order ): 64 | md = { 65 | "MsgType":"X", 66 | "MDBkTyp": '3', # Order Depth 67 | "MDIncGrp": [{ 68 | "MDUpdateAction":"0", # new 69 | "Symbol": symbol, 70 | "MDEntryType": entry_type, 71 | "MDEntryPositionNo": order_position + 1, 72 | "MDEntryID": order.id, 73 | "MDEntryPx": order.price, 74 | "MDEntrySize": order.leaves_qty, 75 | "MDEntryDate": order.created.date(), 76 | "MDEntryTime": order.created.time(), 77 | "OrderID": order.id, 78 | "UserID": order.account_id, 79 | "Username": order.account_username, 80 | "Broker": order.broker_username 81 | 82 | }] 83 | } 84 | TradeApplication.instance().publish( 'MD_INCREMENTAL_' + symbol + '.' + entry_type , md ) 85 | 86 | @staticmethod 87 | def publish_trades(symbol, trades): 88 | md_trades = [] 89 | for trade in trades: 90 | md_trades.append({ 91 | "MDUpdateAction":"0", 92 | "MDEntryType": "2", # Trade 93 | "Symbol": trade.symbol, 94 | "MDEntryPx": trade.price, 95 | "MDEntrySize": trade.size, 96 | "MDEntryDate": trade.created.date(), 97 | "MDEntryTime": trade.created.time(), 98 | "OrderID": trade.order_id, 99 | "Side": trade.side, 100 | "SecondaryOrderID": trade.counter_order_id, 101 | "TradeID": trade.id, 102 | "MDEntryBuyerID": trade.buyer_id, 103 | "MDEntrySellerID": trade.seller_id, 104 | "MDEntryBuyer": trade.buyer_username, 105 | "MDEntrySeller": trade.seller_username, 106 | }) 107 | md = { 108 | "MsgType":"X", 109 | "MDBkTyp": '3', # Order Depth 110 | "MDIncGrp": md_trades 111 | } 112 | TradeApplication.instance().publish( 'MD_TRADE_' + symbol , md ) 113 | 114 | @staticmethod 115 | def generate_trade_history( session, page_size = None, offset = None, sort_column = None, sort_order='ASC' ): 116 | trades = Trade.get_last_trades(session, page_size, offset, sort_column, sort_order) 117 | trade_list = [] 118 | for trade in trades: 119 | trade_list.append([ 120 | trade.id, 121 | trade.symbol, 122 | trade.side, 123 | trade.price, 124 | trade.size, 125 | trade.buyer_id, 126 | trade.seller_id, 127 | trade.buyer_username, 128 | trade.seller_username, 129 | trade.created, 130 | trade.order_id, 131 | trade.counter_order_id 132 | ]) 133 | return trade_list 134 | 135 | #trades = Trade.get_last_trades( session, since ).all() 136 | #return trades 137 | pass 138 | 139 | @staticmethod 140 | def generate_md_full_refresh( session, symbol, market_depth, om, entries, req_id, timestamp ): 141 | entry_list = [] 142 | 143 | for entry_type in entries: 144 | if entry_type == '0' or entry_type == '1': 145 | if entry_type == '0': # Bid 146 | orders = om.buy_side 147 | else: # Offer 148 | orders = om.sell_side 149 | 150 | entry_position = 0 151 | for order in orders: 152 | if order.type == '1': # Hide the market orders 153 | continue 154 | 155 | entry_position += 1 156 | 157 | entry_list.append({ 158 | "MDEntryType": entry_type, 159 | "MDEntryPositionNo": entry_position, 160 | "MDEntryID": order.id, 161 | "MDEntryPx": order.price, 162 | "MDEntrySize": order.leaves_qty, 163 | "MDEntryDate": order.created.date(), 164 | "MDEntryTime": order.created.time(), 165 | "OrderID": order.id, 166 | "UserID": order.account_id, 167 | "Username": order.account_username, 168 | "Broker": order.broker_username 169 | }) 170 | 171 | if entry_position >= market_depth > 0: 172 | break 173 | 174 | md = { 175 | "MsgType":"W", 176 | "MDReqID": req_id, 177 | "MarketDepth": market_depth, 178 | "Symbol": symbol, 179 | "MDFullGrp": entry_list 180 | } 181 | TradeApplication.instance().publish( 'MD_FULL_REFRESH_' + symbol , md ) 182 | return md 183 | -------------------------------------------------------------------------------- /match/session.py: -------------------------------------------------------------------------------- 1 | 2 | from errors import * 3 | from views import * 4 | from trade_application import TradeApplication 5 | 6 | class Session(object): 7 | def __init__(self, session_id, remote_ip=None, client_version=None): 8 | self.session_id = session_id 9 | self.remote_ip = remote_ip 10 | self.client_version = client_version 11 | 12 | self.user = None 13 | self.is_broker = False 14 | self.profile = None 15 | self.broker = None 16 | self.should_end = False 17 | self.user_accounts = None 18 | self.broker_accounts = None 19 | self.email_lang = TradeApplication.instance().options.global_email_language 20 | self.permission_list = {'*':[]} 21 | 22 | def set_user(self, user, permission_list): 23 | if user is None: 24 | self.user = None 25 | self.is_broker = False 26 | self.profile = None 27 | self.should_end = False 28 | self.email_lang = TradeApplication.instance().options.global_email_language 29 | return 30 | 31 | if self.user: 32 | raise UserAlreadyLogged 33 | self.user = user 34 | self.email_lang = user.email_lang 35 | self.permission_list = permission_list 36 | self.is_broker = self.user.is_broker 37 | 38 | from models import Broker 39 | if self.is_broker: 40 | self.profile = Broker.get_broker( TradeApplication.instance().db_session,user.id) 41 | self.user_accounts = json.loads(self.profile.accounts) 42 | else: 43 | self.profile = user 44 | 45 | if user.broker_id is not None: 46 | self.broker = Broker.get_broker( TradeApplication.instance().db_session,user.broker_id) 47 | if self.broker is not None: 48 | self.broker_accounts = json.loads(self.broker.accounts) 49 | 50 | 51 | def has_access_to_account_info(self): 52 | return '*' in self.permission_list or 'BF' in self.permission_list 53 | 54 | 55 | def process_message(self, msg): 56 | if msg.type == '1': # TestRequest 57 | return processTestRequest(self, msg) 58 | 59 | elif msg.type == 'BE': # login 60 | 61 | reqId = msg.get('UserReqTyp') 62 | if reqId == '1': 63 | return processLogin(self, msg) 64 | 65 | if reqId == '3': 66 | return processChangePassword(self, msg) 67 | 68 | elif msg.type == 'D': # New Order Single 69 | return processNewOrderSingle(self, msg) 70 | 71 | elif msg.type == 'F' : # Cancel Order Request 72 | return processCancelOrderRequest(self, msg) 73 | 74 | elif msg.type == 'x': # Security List Request 75 | return processSecurityListRequest(self, msg) 76 | 77 | elif msg.type == 'U0': # signup 78 | return processSignup(self, msg) 79 | 80 | elif msg.type == 'U2': # Request for Balances 81 | return processRequestForBalances(self, msg) 82 | 83 | elif msg.type == 'U4': # Request for Open Orders 84 | return processRequestForOpenOrders(self, msg) 85 | 86 | elif msg.type == 'U6': # CryptoCoin Withdraw Request 87 | return processWithdrawRequest(self, msg) 88 | 89 | elif msg.type == 'U10': # Request password request 90 | return processRequestPasswordRequest(self, msg) 91 | 92 | elif msg.type == 'U12': # Password request 93 | return processPasswordRequest(self, msg) 94 | 95 | elif msg.type == 'U16': #Enable Disable Two Factor Authentication 96 | return processEnableDisableTwoFactorAuth(self, msg) 97 | 98 | elif msg.type == 'U18': #Request Deposit 99 | return processRequestDeposit(self, msg) 100 | 101 | elif msg.type == 'U20': # Request Deposit Method List 102 | return processRequestDepositMethods(self, msg) 103 | 104 | elif msg.type == 'U48': # Request Deposit Method 105 | return processRequestDepositMethod(self, msg) 106 | 107 | elif msg.type == 'U24': # Withdraw Confirmation Request 108 | return processWithdrawConfirmationRequest(self, msg) 109 | 110 | elif msg.type == 'U26': # Withdraw List Request 111 | return processWithdrawListRequest(self, msg) 112 | 113 | elif msg.type == 'U28': # Request broker lists 114 | return processBrokerListRequest(self, msg) 115 | 116 | elif msg.type == 'U30': # Deposit List Request 117 | return processDepositListRequest(self, msg) 118 | 119 | elif msg.type == 'U34': # Ledger List Request 120 | return processLedgerListRequest(self, msg) 121 | 122 | elif msg.type == 'U36': # Ledger List Request 123 | return processTradersRankRequest(self, msg) 124 | 125 | elif msg.type == 'U38': # Update User Profile 126 | return processUpdateUserProfile(self, msg) 127 | 128 | elif msg.type == 'U42': # Request for Positions 129 | return processRequestForPositions(self,msg) 130 | 131 | elif msg.type == 'U50': # ApiKey List Request 132 | return processApiKeyListRequest(self, msg) 133 | 134 | elif msg.type == 'U52': # ApiKey Create Request 135 | return processApiKeyCreateRequest(self, msg) 136 | 137 | elif msg.type == 'U54': # ApiKey Revoke Request 138 | return processApiKeyRevokeRequest(self, msg) 139 | 140 | elif msg.type == 'U70': # Cancel Withdraw 141 | return processCancelWithdraw(self, msg) 142 | 143 | elif msg.type == 'U78': # Comment Withdraw 144 | return processCommentWithdraw(self, msg) 145 | 146 | elif msg.type == 'B0': # Deposit Payment Confirmation 147 | return processProcessDeposit(self, msg) 148 | 149 | elif msg.type == 'B2': # Customer List Request 150 | return processCustomerListRequest(self, msg) 151 | 152 | elif msg.type == 'B4': # Customer Detail Request 153 | return processCustomerDetailRequest(self, msg) 154 | 155 | elif msg.type == 'B6': # Process Withdraw 156 | return processProcessWithdraw(self, msg) 157 | 158 | elif msg.type == 'B8': # Verify Customer 159 | return processVerifyCustomer(self, msg) 160 | 161 | elif msg.type == 'A0': # Request Query in Database 162 | return processRequestDatabaseQuery(self, msg) 163 | raise InvalidMessageError() 164 | -------------------------------------------------------------------------------- /match/session_manager.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | from session import Session 5 | from errors import * 6 | 7 | class SessionManager(object): 8 | def __init__(self, timeout_limit=0): 9 | self.sessions = {} 10 | self.timeout_limit = timeout_limit 11 | 12 | self.publish_queue = [] 13 | 14 | def open_session(self, session_id, msg): 15 | if session_id in self.sessions: 16 | return 'CLS,' + session_id 17 | 18 | remote_ip = None 19 | client_version = None 20 | if msg: 21 | remote_ip = msg.get('RemoteIp', None) 22 | client_version = msg.get('ClientVersion', None) 23 | 24 | self.sessions[session_id] = [0, datetime.datetime.now(), Session( session_id, remote_ip, client_version ) ] 25 | return 'OPN,' + session_id 26 | 27 | def close_session(self, session_id): 28 | if session_id not in self.sessions: 29 | return 'CLS,' + session_id 30 | del self.sessions[session_id] 31 | return 'CLS,' + session_id 32 | 33 | def process_message(self, msg_header, session_id, msg): 34 | if msg_header == 'OPN': 35 | return self.open_session(session_id, msg) 36 | 37 | elif msg_header == 'CLS': 38 | return self.close_session(session_id) 39 | 40 | # wrong opt_code 41 | if msg_header != 'REQ': 42 | self.close_session(session_id) 43 | raise InvalidOptCodeError() 44 | 45 | # wrong session id 46 | if session_id not in self.sessions: 47 | self.close_session(session_id) 48 | raise InvalidSessionError() 49 | 50 | 51 | # Check if the session is expired 52 | session_time = self.sessions[session_id][1] 53 | if self.timeout_limit: 54 | if datetime.timedelta(seconds=self.timeout_limit) + session_time < datetime.datetime.now(): 55 | raise SessionTimeoutError() 56 | 57 | self.sessions[session_id][0] += 1 # increment the number of received messages 58 | self.sessions[session_id][1] = datetime.datetime.now() # update session time, so we can timeout old sessions. 59 | 60 | if not msg: 61 | raise InvalidMessageError() 62 | 63 | session = self.sessions[session_id][2] 64 | response = session.process_message(msg) 65 | 66 | if session.should_end: 67 | self.close_session(session_id) 68 | return 'CLS,' + response 69 | 70 | return 'REP,' + response 71 | 72 | -------------------------------------------------------------------------------- /match/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from test_model import * -------------------------------------------------------------------------------- /match/trade_application.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import logging 4 | import logging.handlers 5 | import zmq 6 | import time 7 | import datetime 8 | import traceback 9 | 10 | from sqlalchemy import create_engine 11 | from sqlalchemy.orm import scoped_session, sessionmaker 12 | import json 13 | from json_encoder import JsonEncoder 14 | 15 | from errors import * 16 | 17 | class TradeApplication(object): 18 | @classmethod 19 | def instance(cls): 20 | if not hasattr(cls, "_instance"): 21 | cls._instance = cls() 22 | return cls._instance 23 | 24 | def initialize(self, options, instance_name): 25 | self.publish_queue = [] 26 | self.options = options 27 | self.instance_name = instance_name 28 | 29 | self.order_matcher_disabled = False 30 | if options.has_option('order_matcher_disabled'): 31 | self.order_matcher_disabled = True 32 | 33 | from models import Base, db_bootstrap 34 | db_engine = options.sqlalchemy_engine + ':///' + \ 35 | os.path.expanduser(options.sqlalchemy_connection_string) 36 | engine = create_engine( db_engine, echo=options.db_echo) 37 | Base.metadata.create_all(engine) 38 | 39 | 40 | self.db_session = scoped_session(sessionmaker(bind=engine)) 41 | db_bootstrap(self.db_session) 42 | 43 | from session_manager import SessionManager 44 | self.session_manager = SessionManager(timeout_limit=self.options.session_timeout_limit) 45 | 46 | self.context = zmq.Context() 47 | self.input_socket = self.context.socket(zmq.REP) 48 | self.input_socket.bind(self.options.trade_in) 49 | 50 | self.publisher_socket = self.context.socket(zmq.PUB) 51 | self.publisher_socket.bind(self.options.trade_pub) 52 | 53 | input_log_file_handler = logging.handlers.TimedRotatingFileHandler( 54 | os.path.expanduser(self.options.trade_log), when='MIDNIGHT') 55 | input_log_file_handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s')) 56 | 57 | self.replay_logger = logging.getLogger(self.instance_name) 58 | self.replay_logger.setLevel(logging.INFO) 59 | self.replay_logger.addHandler(input_log_file_handler) 60 | 61 | ch = logging.StreamHandler(sys.stdout) 62 | ch.setLevel(logging.DEBUG) 63 | ch.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 64 | self.replay_logger.addHandler(ch) 65 | 66 | self.replay_logger.info('START') 67 | 68 | self.log_start_data() 69 | 70 | 71 | 72 | def log(self, command, key, value=None): 73 | if len(logging.getLogger().handlers): 74 | logging.getLogger().handlers = [] # workaround to avoid stdout logging from the root logger 75 | 76 | log_msg = command + ',' + key 77 | if value: 78 | try: 79 | log_msg += ',' + value 80 | except Exception,e : 81 | try: 82 | log_msg += ',' + str(value) 83 | except Exception,e : 84 | try: 85 | log_msg += ',' + unicode(value) 86 | except Exception,e : 87 | log_msg += ', [object]' 88 | 89 | self.replay_logger.info( log_msg ) 90 | 91 | def log_start_data(self): 92 | self.log('PARAM','BEGIN') 93 | self.log('PARAM','trade_in' ,self.options.trade_in) 94 | self.log('PARAM','trade_pub' ,self.options.trade_pub) 95 | self.log('PARAM','trade_log' ,self.options.trade_log) 96 | self.log('PARAM','session_timeout_limit' ,self.options.session_timeout_limit) 97 | self.log('PARAM','db_echo' ,self.options.db_echo) 98 | self.log('PARAM','sqlalchemy_engine' ,self.options.sqlalchemy_engine) 99 | self.log('PARAM','sqlalchemy_connection_string' ,self.options.sqlalchemy_connection_string) 100 | self.log('PARAM','test_mode' ,self.options.test_mode) 101 | self.log('PARAM','dev_mode' ,self.options.dev_mode) 102 | self.log('PARAM','satoshi_mode' ,self.options.satoshi_mode) 103 | self.log('PARAM','order_matcher_disabled' ,self.order_matcher_disabled) 104 | self.log('PARAM','global_email_language' ,self.options.global_email_language) 105 | self.log('PARAM','END') 106 | 107 | 108 | from models import User, Deposit, DepositMethods, Order, Withdraw, Broker, \ 109 | Currency, Instrument, ApiAccess, Balance, Position, GreenAddresses 110 | 111 | green_address_list = self.db_session.query(GreenAddresses) 112 | for green_address_entity in green_address_list: 113 | self.log('DB_ENTITY', 'GREEN_ADDRESS', green_address_entity) 114 | 115 | currencies = self.db_session.query(Currency) 116 | for currency in currencies: 117 | self.log('DB_ENTITY', 'CURRENCY', currency) 118 | 119 | instruments = self.db_session.query(Instrument) 120 | for instrument in instruments: 121 | self.log('DB_ENTITY', 'INSTRUMENT', instrument) 122 | 123 | users = self.db_session.query(User) 124 | for user in users: 125 | self.log('DB_ENTITY', 'USER', user) 126 | 127 | api_access_list = self.db_session.query(ApiAccess) 128 | for api_access_entity in api_access_list: 129 | self.log('DB_ENTITY', 'API_ACCESS', api_access_entity) 130 | 131 | # log all users on the replay log 132 | brokers = self.db_session.query(Broker) 133 | for broker in brokers: 134 | Broker.cache_broker(broker.id, broker) 135 | self.log('DB_ENTITY', 'BROKER', broker) 136 | 137 | deposit_options = self.db_session.query(DepositMethods) 138 | for deposit_option in deposit_options: 139 | self.log('DB_ENTITY', 'DEPOSIT_OPTION', deposit_option) 140 | 141 | deposits = self.db_session.query(Deposit) 142 | for deposit in deposits: 143 | self.log('DB_ENTITY', 'DEPOSIT', repr(deposit)) 144 | 145 | withdraws = self.db_session.query(Withdraw) 146 | for withdraw in withdraws: 147 | self.log('DB_ENTITY', 'WITHDRAW', withdraw ) 148 | 149 | balance_list = self.db_session.query(Balance) 150 | for balance_entity in balance_list: 151 | self.log('DB_ENTITY', 'BALANCE', balance_entity ) 152 | 153 | position_list = self.db_session.query(Position) 154 | for position_entity in position_list: 155 | self.log('DB_ENTITY', 'POSITION', position_entity ) 156 | 157 | orders = self.db_session.query(Order).filter(Order.status.in_(("0", "1"))).order_by(Order.created) 158 | for order in orders: 159 | self.log('DB_ENTITY','ORDER',order) 160 | 161 | def publish(self, key, data): 162 | print key, data 163 | self.publish_queue.append([ key, data ]) 164 | 165 | def run(self): 166 | from message import JsonMessage, InvalidMessageException 167 | from market_data_publisher import MarketDataPublisher 168 | from execution import OrderMatcher 169 | from models import Order 170 | 171 | orders = self.db_session.query(Order).filter(Order.status.in_(("0", "1"))).order_by(Order.created) 172 | for order in orders: 173 | OrderMatcher.get( order.symbol ).match(self.db_session, order, self.order_matcher_disabled) 174 | 175 | while True: 176 | raw_message = self.input_socket.recv() 177 | 178 | msg_header = raw_message[:3] 179 | session_id = raw_message[4:20] 180 | json_raw_message = raw_message[21:].strip() 181 | 182 | try: 183 | msg = None 184 | if json_raw_message: 185 | try: 186 | msg = JsonMessage(json_raw_message) 187 | except InvalidMessageException, e: 188 | self.log('IN', 'TRADE_IN_REQ_ERROR', raw_message) 189 | raise InvalidMessageError() 190 | 191 | # never write passwords in the log file 192 | if msg.has('Password'): 193 | raw_message = raw_message.replace(msg.get('Password'), '*') 194 | if msg.has('NewPassword'): 195 | raw_message = raw_message.replace(msg.get('NewPassword'), '*') 196 | 197 | self.log('IN', 'TRADE_IN_REQ' ,raw_message ) 198 | 199 | if msg: 200 | if msg.isMarketDataRequest(): # Market Data Request 201 | req_id = msg.get('MDReqID') 202 | market_depth = msg.get('MarketDepth') 203 | instruments = msg.get('Instruments') 204 | entries = msg.get('MDEntryTypes') 205 | transact_time = msg.get('TransactTime') 206 | 207 | timestamp = None 208 | if transact_time: 209 | timestamp = transact_time 210 | else: 211 | trade_date = msg.get('TradeDate') 212 | if not trade_date: 213 | trade_date = time.strftime("%Y%m%d", time.localtime()) 214 | 215 | self.log('OUT', 'TRADEDATE', trade_date) 216 | timestamp = datetime.datetime.strptime(trade_date, "%Y%m%d") 217 | 218 | self.log('OUT', 'TIMESTAMP', timestamp ) 219 | 220 | if len(instruments) > 1: 221 | raise InvalidMessageError() 222 | 223 | instrument = instruments[0] 224 | 225 | om = OrderMatcher.get(instrument) 226 | response_message = MarketDataPublisher.generate_md_full_refresh( self.db_session, instrument, market_depth, om, entries, req_id, timestamp ) 227 | response_message = 'REP,' + json.dumps( response_message , cls=JsonEncoder) 228 | elif msg.isTradeHistoryRequest(): 229 | 230 | page = msg.get('Page', 0) 231 | page_size = msg.get('PageSize', 100) 232 | offset = page * page_size 233 | 234 | columns = [ 'TradeID' , 'Market', 'Side', 'Price', 'Size', 235 | 'BuyerID' , 'SellerID', 'BuyerUsername' ,'SellerUsername', 'Created', 236 | 'OrderId' , 'CounterOrderID'] 237 | 238 | trade_list = MarketDataPublisher.generate_trade_history(self.db_session, page_size, offset ) 239 | 240 | response_message = 'REP,' + json.dumps({ 241 | 'MsgType' : 'U33', # TradeHistoryResponse 242 | 'TradeHistoryReqID' : -1, 243 | 'Page' : page, 244 | 'PageSize' : page_size, 245 | 'Columns' : columns, 246 | 'TradeHistoryGrp' : trade_list 247 | }, cls=JsonEncoder) 248 | 249 | else: 250 | response_message = self.session_manager.process_message( msg_header, session_id, msg ) 251 | else: 252 | response_message = self.session_manager.process_message( msg_header, session_id, msg ) 253 | 254 | except TradeRuntimeError, e: 255 | self.db_session.rollback() 256 | self.session_manager.close_session(session_id) 257 | response_message = 'ERR,{"MsgType":"ERROR", "Description":"' + e.error_description.replace("'", "") + '", "Detail": ""}' 258 | 259 | except Exception,e: 260 | traceback.print_exc() 261 | self.db_session.rollback() 262 | self.session_manager.close_session(session_id) 263 | response_message = 'ERR,{"MsgType":"ERROR", "Description":"Unknow error", "Detail": "' + str(e) + '"}' 264 | 265 | # send the response 266 | self.log('OUT', 'TRADE_IN_REP', response_message ) 267 | self.input_socket.send_unicode(response_message) 268 | 269 | # publish all publications 270 | for key, message in self.publish_queue: 271 | self.log('OUT', 'TRADE_PUB', str([ key, message]) ) 272 | self.publisher_socket.send_multipart( [ '^' + str(key) + '$' , json.dumps(message, cls=JsonEncoder)] ) 273 | self.publish_queue = [] 274 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | RUN rm /etc/nginx/nginx.conf 3 | ADD nginx.conf /etc/nginx/nginx.conf 4 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | #user nobody; 2 | worker_processes 1; 3 | 4 | events { 5 | worker_connections 1024; 6 | } 7 | 8 | http { 9 | include mime.types; 10 | default_type application/octet-stream; 11 | 12 | sendfile on; 13 | #tcp_nopush on; 14 | 15 | keepalive_timeout 650; 16 | 17 | gzip on; 18 | 19 | 20 | map $http_upgrade $connection_upgrade { 21 | default upgrade; 22 | '' close; 23 | } 24 | 25 | upstream ws_gateway { 26 | server 10.5.0.20:8445; 27 | } 28 | 29 | upstream api_gateway { 30 | server 10.5.0.30:9943; 31 | } 32 | 33 | server { 34 | listen 80; 35 | location / { 36 | return 301 https://$host$request_uri; 37 | } 38 | 39 | location /api/ { 40 | proxy_pass http://api_gateway; 41 | proxy_set_header X-Real-IP $remote_addr; 42 | proxy_set_header Host $host; 43 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 44 | proxy_redirect off; 45 | } 46 | } 47 | 48 | server { 49 | listen 443; 50 | server_name brabex.com.br; 51 | #server_name bitex.com.br; 52 | 53 | ssl on; 54 | ssl_certificate /opt/bitex/ssl/server.crt; 55 | ssl_certificate_key /opt/bitex/ssl/server.key; 56 | 57 | ssl_session_timeout 5m; 58 | 59 | ssl_protocols SSLv2 SSLv3 TLSv1; 60 | ssl_ciphers HIGH:!aNULL:!MD5; 61 | ssl_prefer_server_ciphers on; 62 | 63 | 64 | location / { 65 | root /opt/bitex/static/; 66 | index index.html; 67 | 68 | location ~ /(profile|ranking|market|broker_application|my_broker|set_new_password|signin|signup|forgot_password|tos|start|trading|offerbook|deposit|withdraw|account_activity|customers|account_overview|verification|enable_two_factor|ledger|withdraw_requests|deposit_requests)$ { 69 | rewrite /(.*) /blinktrade.html break; 70 | } 71 | 72 | location /account_overview/ { 73 | rewrite /(.*) /blinktrade.html break; 74 | } 75 | 76 | location /_webhook/ { 77 | proxy_pass http://ws_gateway; 78 | proxy_set_header X-Real-IP $remote_addr; 79 | proxy_set_header Host $host; 80 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 81 | proxy_redirect off; 82 | } 83 | 84 | location /api/ { 85 | proxy_pass http://ws_gateway; 86 | proxy_set_header X-Real-IP $remote_addr; 87 | proxy_set_header Host $host; 88 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 89 | proxy_redirect off; 90 | } 91 | 92 | location /get_deposit/ { 93 | proxy_pass http://ws_gateway; 94 | proxy_set_header X-Real-IP $remote_addr; 95 | proxy_set_header Host $host; 96 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 97 | 98 | rewrite /get_deposit(.*) /get_deposit$1 break; 99 | proxy_redirect off; 100 | } 101 | 102 | location /process_deposit/ { 103 | proxy_pass http://ws_gateway; 104 | proxy_set_header X-Real-IP $remote_addr; 105 | proxy_set_header Host $host; 106 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 107 | proxy_redirect off; 108 | } 109 | 110 | 111 | location /trade/ { 112 | rewrite /trade/(.*) /$1 break; 113 | 114 | proxy_pass http://ws_gateway; 115 | proxy_http_version 1.1; 116 | proxy_redirect off; 117 | 118 | proxy_set_header Host $host; 119 | proxy_set_header X-Real-IP $remote_addr; 120 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 121 | proxy_set_header Upgrade $http_upgrade; 122 | proxy_set_header Connection $connection_upgrade; 123 | proxy_read_timeout 3000s; 124 | } 125 | 126 | 127 | error_page 404 /404.html; 128 | location = /40x.html { 129 | root /opt/bitex/static/; 130 | } 131 | 132 | 133 | # redirect server error pages to the static page /50x.html 134 | # 135 | error_page 500 502 503 504 /50x.html; 136 | location = /50x.html { 137 | root /opt/bitex/static/; 138 | } 139 | } 140 | } 141 | } 142 | 143 | -------------------------------------------------------------------------------- /receiver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | ADD requirements.txt /tmp/requirements.txt 3 | RUN pip install -r /tmp/requirements.txt 4 | ADD receiver /code 5 | ADD receiver/api_receive.ini /code 6 | ADD libraries /libraries 7 | WORKDIR /code 8 | ENTRYPOINT ["python"] 9 | CMD [ "main.py", "-c", "api_receive.ini"] 10 | 11 | -------------------------------------------------------------------------------- /receiver/api_receive.ini: -------------------------------------------------------------------------------- 1 | [testnet] 2 | db_engine = sqlite:////opt/bitex/db/api_receive.development.sqlite 3 | db_echo=True 4 | log = /opt/bitex/logs/api_receive.log 5 | port = 9943 6 | rpchost=10.5.0.50 7 | rpcport=18332 8 | rpcuser=bitcoinrpc 9 | rpcpassword=senha123 10 | rpc_url = http://%(rpcuser)s:%(rpcpassword)s@%(rpchost)s:%(rpcport)s 11 | -------------------------------------------------------------------------------- /receiver/api_receive_application.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | import logging 3 | 4 | import tornado.ioloop 5 | import tornado.web 6 | import sys 7 | 8 | from tornado import httpclient 9 | from functools import partial 10 | 11 | from sqlalchemy import create_engine, func 12 | from sqlalchemy.orm import scoped_session, sessionmaker 13 | 14 | from create_receive_handler import ReceiveHandler 15 | from wallet_notify_handler import WalletNotifyHandler 16 | from block_notify_handler import BlockNotifyHandler 17 | 18 | from authproxy import AuthServiceProxy 19 | 20 | class ApiReceiveApplication(tornado.web.Application): 21 | def __init__(self, options, instance_name): 22 | self.options = options 23 | self.instance_name = instance_name 24 | handlers = [ 25 | (r"/api/receive", ReceiveHandler), 26 | (r"/api/walletnotify/(?P[^\/]+)", WalletNotifyHandler), 27 | (r"/api/blocknotify/(?P[^\/]+)", BlockNotifyHandler), 28 | ] 29 | settings = dict( 30 | cookie_secret='cookie_secret' 31 | ) 32 | tornado.web.Application.__init__(self, handlers, **settings) 33 | 34 | input_log_file_handler = logging.handlers.TimedRotatingFileHandler( self.options.log, when='MIDNIGHT') 35 | formatter = logging.Formatter('%(asctime)s - %(message)s') 36 | input_log_file_handler.setFormatter(formatter) 37 | 38 | self.bitcoind = AuthServiceProxy(self.options.rpc_url ) 39 | self.paytxfee = self.bitcoind.getinfo()['paytxfee'] 40 | 41 | 42 | self.replay_logger = logging.getLogger(self.instance_name) 43 | self.replay_logger.setLevel(logging.DEBUG) 44 | self.replay_logger.addHandler(input_log_file_handler) 45 | self.replay_logger.info('START') 46 | 47 | ch = logging.StreamHandler(sys.stdout) 48 | ch.setLevel(logging.DEBUG) 49 | ch.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 50 | self.replay_logger.addHandler(ch) 51 | 52 | 53 | from models import Base, db_bootstrap 54 | engine = create_engine( self.options.db_engine, echo=self.options.db_echo) 55 | Base.metadata.create_all(engine) 56 | self.db_session = scoped_session(sessionmaker(bind=engine)) 57 | db_bootstrap(self.db_session) 58 | 59 | self.log_start_data() 60 | 61 | def invoke_callback_url(self, forwarding_address): 62 | url = forwarding_address.get_callback_url() 63 | self.log('EXECUTE', 'curl ' + url) 64 | context = ssl._create_unverified_context() 65 | http_client = httpclient.AsyncHTTPClient(defaults=dict(ssl_options=context)) 66 | http_client.fetch(url, partial(self.on_handle_callback_url, forwarding_address.id )) 67 | 68 | 69 | def on_handle_callback_url(self, forwarding_address_id, response ): 70 | from models import ForwardingAddress 71 | forwarding_address = ForwardingAddress.get_by_id(self.db_session, forwarding_address_id) 72 | 73 | if response.error: 74 | self.log('ERROR', str(response.error)) 75 | forwarding_address.callback_number_of_errors += 1 76 | self.db_session.add(forwarding_address) 77 | self.db_session.commit() 78 | else: 79 | if response.body == '*ok*': 80 | forwarding_address.is_confirmed_by_client = True 81 | self.db_session.add(forwarding_address) 82 | self.db_session.commit() 83 | 84 | 85 | def log(self, command, key, value=None): 86 | #if len(logging.getLogger().handlers): 87 | # logging.getLogger().handlers = [] # workaround to avoid stdout logging from the root logger 88 | 89 | log_msg = command + ',' + key 90 | if value: 91 | try: 92 | log_msg += ',' + value 93 | except Exception as e : 94 | try: 95 | log_msg += ',' + str(value) 96 | except Exception as e : 97 | try: 98 | log_msg += ',' + str(value) 99 | except Exception as e : 100 | log_msg += ', [object]' 101 | 102 | 103 | self.replay_logger.info( log_msg ) 104 | 105 | 106 | def log_start_data(self): 107 | self.log('PARAM','BEGIN') 108 | self.log('PARAM','port' ,self.options.port) 109 | self.log('PARAM','log' ,self.options.log) 110 | self.log('PARAM','db_echo' ,self.options.db_echo) 111 | self.log('PARAM','db_engine' ,self.options.db_engine) 112 | self.log('PARAM','rpc_url' ,self.options.rpc_url) 113 | self.log('PARAM','END') 114 | 115 | from models import ForwardingAddress 116 | fwd_address_list = self.db_session.query(ForwardingAddress) 117 | for fwd_address in fwd_address_list: 118 | self.log('DB_ENTITY', 'FORWARDING_ADDRESS', fwd_address) 119 | 120 | bitcoin_info = self.bitcoind.getinfo() 121 | self.log('INFO', 'BITCOIND_GETINFO', str(bitcoin_info)) 122 | 123 | def clean_up(self): 124 | pass 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /receiver/authproxy.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Jeff Garzik 2 | # 3 | # Previous copyright, from python-jsonrpc/jsonrpc/proxy.py: 4 | # 5 | # Copyright (c) 2007 Jan-Klaas Kollhof 6 | # 7 | # This file is part of jsonrpc. 8 | # 9 | # jsonrpc is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU Lesser General Public License as published by 11 | # the Free Software Foundation; either version 2.1 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This software is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Lesser General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Lesser General Public License 20 | # along with this software; if not, write to the Free Software 21 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 22 | """HTTP proxy for opening RPC connection to bitcoind. 23 | 24 | AuthServiceProxy has the following improvements over python-jsonrpc's 25 | ServiceProxy class: 26 | 27 | - HTTP connections persist for the life of the AuthServiceProxy object 28 | (if server supports HTTP/1.1) 29 | - sends protocol 'version', per JSON-RPC 1.1 30 | - sends proper, incrementing 'id' 31 | - sends Basic HTTP authentication headers 32 | - parses all JSON numbers that look like floats as Decimal 33 | - uses standard Python json lib 34 | """ 35 | 36 | import base64 37 | import decimal 38 | import http.client 39 | import json 40 | import logging 41 | import socket 42 | import time 43 | import urllib.parse 44 | 45 | HTTP_TIMEOUT = 30 46 | USER_AGENT = "AuthServiceProxy/0.1" 47 | 48 | log = logging.getLogger("BitcoinRPC") 49 | 50 | class JSONRPCException(Exception): 51 | def __init__(self, rpc_error): 52 | try: 53 | errmsg = '%(message)s (%(code)i)' % rpc_error 54 | except (KeyError, TypeError): 55 | errmsg = '' 56 | super().__init__(errmsg) 57 | self.error = rpc_error 58 | 59 | 60 | def EncodeDecimal(o): 61 | if isinstance(o, decimal.Decimal): 62 | return str(o) 63 | raise TypeError(repr(o) + " is not JSON serializable") 64 | 65 | class AuthServiceProxy(): 66 | __id_count = 0 67 | 68 | # ensure_ascii: escape unicode as \uXXXX, passed to json.dumps 69 | def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None, ensure_ascii=True): 70 | self.__service_url = service_url 71 | self._service_name = service_name 72 | self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests 73 | self.__url = urllib.parse.urlparse(service_url) 74 | port = 80 if self.__url.port is None else self.__url.port 75 | user = None if self.__url.username is None else self.__url.username.encode('utf8') 76 | passwd = None if self.__url.password is None else self.__url.password.encode('utf8') 77 | authpair = user + b':' + passwd 78 | self.__auth_header = b'Basic ' + base64.b64encode(authpair) 79 | 80 | if connection: 81 | # Callables re-use the connection of the original proxy 82 | self.__conn = connection 83 | elif self.__url.scheme == 'https': 84 | self.__conn = http.client.HTTPSConnection(self.__url.hostname, port, timeout=timeout) 85 | else: 86 | self.__conn = http.client.HTTPConnection(self.__url.hostname, port, timeout=timeout) 87 | 88 | def __getattr__(self, name): 89 | if name.startswith('__') and name.endswith('__'): 90 | # Python internal stuff 91 | raise AttributeError 92 | if self._service_name is not None: 93 | name = "%s.%s" % (self._service_name, name) 94 | return AuthServiceProxy(self.__service_url, name, connection=self.__conn) 95 | 96 | def _request(self, method, path, postdata): 97 | ''' 98 | Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout). 99 | This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5. 100 | ''' 101 | headers = {'Host': self.__url.hostname, 102 | 'User-Agent': USER_AGENT, 103 | 'Authorization': self.__auth_header, 104 | 'Content-type': 'application/json'} 105 | try: 106 | self.__conn.request(method, path, postdata, headers) 107 | return self._get_response() 108 | except http.client.BadStatusLine as e: 109 | if e.line == "''": # if connection was closed, try again 110 | self.__conn.close() 111 | self.__conn.request(method, path, postdata, headers) 112 | return self._get_response() 113 | else: 114 | raise 115 | except (BrokenPipeError, ConnectionResetError): 116 | # Python 3.5+ raises BrokenPipeError instead of BadStatusLine when the connection was reset 117 | # ConnectionResetError happens on FreeBSD with Python 3.4 118 | self.__conn.close() 119 | self.__conn.request(method, path, postdata, headers) 120 | return self._get_response() 121 | 122 | def get_request(self, *args, **argsn): 123 | AuthServiceProxy.__id_count += 1 124 | 125 | log.debug("-%s-> %s %s" % (AuthServiceProxy.__id_count, self._service_name, 126 | json.dumps(args, default=EncodeDecimal, ensure_ascii=self.ensure_ascii))) 127 | if args and argsn: 128 | raise ValueError('Cannot handle both named and positional arguments') 129 | return {'version': '1.1', 130 | 'method': self._service_name, 131 | 'params': args or argsn, 132 | 'id': AuthServiceProxy.__id_count} 133 | 134 | def __call__(self, *args, **argsn): 135 | postdata = json.dumps(self.get_request(*args, **argsn), default=EncodeDecimal, ensure_ascii=self.ensure_ascii) 136 | response = self._request('POST', self.__url.path, postdata.encode('utf-8')) 137 | if response['error'] is not None: 138 | raise JSONRPCException(response['error']) 139 | elif 'result' not in response: 140 | raise JSONRPCException({ 141 | 'code': -343, 'message': 'missing JSON-RPC result'}) 142 | else: 143 | return response['result'] 144 | 145 | def batch(self, rpc_call_list): 146 | postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii) 147 | log.debug("--> " + postdata) 148 | return self._request('POST', self.__url.path, postdata.encode('utf-8')) 149 | 150 | def _get_response(self): 151 | req_start_time = time.time() 152 | try: 153 | http_response = self.__conn.getresponse() 154 | except socket.timeout as e: 155 | raise JSONRPCException({ 156 | 'code': -344, 157 | 'message': '%r RPC took longer than %f seconds. Consider ' 158 | 'using larger timeout for calls that take ' 159 | 'longer to return.' % (self._service_name, 160 | self.__conn.timeout)}) 161 | if http_response is None: 162 | raise JSONRPCException({ 163 | 'code': -342, 'message': 'missing HTTP response from server'}) 164 | 165 | content_type = http_response.getheader('Content-Type') 166 | if content_type != 'application/json': 167 | raise JSONRPCException({ 168 | 'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)}) 169 | 170 | responsedata = http_response.read().decode('utf8') 171 | response = json.loads(responsedata, parse_float=decimal.Decimal) 172 | elapsed = time.time() - req_start_time 173 | if "error" in response and response["error"] is None: 174 | log.debug("<-%s- [%.6f] %s" % (response["id"], elapsed, json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii))) 175 | else: 176 | log.debug("<-- [%.6f] %s" % (elapsed, responsedata)) 177 | return response 178 | 179 | def __truediv__(self, relative_uri): 180 | return AuthServiceProxy("{}/{}".format(self.__service_url, relative_uri), self._service_name, connection=self.__conn) 181 | -------------------------------------------------------------------------------- /receiver/block_notify_handler.py: -------------------------------------------------------------------------------- 1 | import tornado.web 2 | import tornado.httpclient 3 | 4 | from tornado import httpclient 5 | from functools import partial 6 | 7 | class BlockNotifyHandler(tornado.web.RequestHandler): 8 | def get(self, hash): 9 | self.application.log('HTTP_GET', '/api/blocknotify/' + hash ) 10 | from models import ForwardingAddress 11 | 12 | unconfirmed_forwarding_addresses = ForwardingAddress.get_unconfirmed_by_client(self.application.db_session) 13 | 14 | should_commit = False 15 | for unconfirmed_forwarding_address in unconfirmed_forwarding_addresses: 16 | try: 17 | self.application.log('DEBUG', 'invoking bitcoind gettransaction ' + unconfirmed_forwarding_address.input_transaction_hash ) 18 | tx = self.application.bitcoind.gettransaction(unconfirmed_forwarding_address.input_transaction_hash) 19 | 20 | unconfirmed_forwarding_address.confirmations = tx['confirmations'] 21 | unconfirmed_forwarding_address.confirm_callback_attempt += 1 22 | self.application.db_session.add(unconfirmed_forwarding_address) 23 | should_commit = True 24 | 25 | self.application.invoke_callback_url(unconfirmed_forwarding_address) 26 | except Exception as e: 27 | self.application.log('ERROR', str(e)) 28 | if should_commit: 29 | self.application.db_session.commit() 30 | 31 | self.write('*ok*') 32 | 33 | -------------------------------------------------------------------------------- /receiver/create_receive_handler.py: -------------------------------------------------------------------------------- 1 | import tornado.web 2 | import tornado.httpclient 3 | 4 | 5 | from tornado.escape import json_encode 6 | 7 | 8 | class ReceiveHandler(tornado.web.RequestHandler): 9 | def get(self, *args, **kwargs): 10 | 11 | method = self.get_argument("method") 12 | address = self.get_argument("address") 13 | callback = self.get_argument("callback") 14 | self.application.log('HTTP_GET', '/api/receive/' + self.request.query ) 15 | 16 | 17 | if method != 'create': 18 | raise tornado.web.MissingArgumentError('method') 19 | 20 | input_address = self.application.bitcoind.getnewaddress('') 21 | 22 | from models import ForwardingAddress 23 | self.application.log('CREATE', 'FORWARDING_ADDRESS', ",".join([address, input_address, callback]) ) 24 | ForwardingAddress.create(self.application.db_session, address, input_address, callback) 25 | 26 | result = { 27 | 'input_address': input_address, 28 | 'fee_percent' : 0, 29 | 'destination' : address, 30 | 'callback_url': callback 31 | } 32 | 33 | self.write(json_encode(result)) 34 | -------------------------------------------------------------------------------- /receiver/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | import argparse 5 | import configparser 6 | from appdirs import site_config_dir 7 | 8 | sys.path.insert(0, os.path.join( os.path.dirname(__file__), '../libraries/' ) ) 9 | 10 | from project_options import ProjectOptions 11 | 12 | import tornado 13 | 14 | from api_receive_application import ApiReceiveApplication 15 | 16 | 17 | def main(): 18 | parser = argparse.ArgumentParser(description="Bitex Api Receive application") 19 | parser.add_argument('-c', "--config", 20 | action="store", 21 | dest="config", 22 | default=os.path.expanduser('~/.bitex/api_receive.ini'), 23 | help='Configuration file', type=str) 24 | arguments = parser.parse_args() 25 | 26 | if not arguments.config: 27 | parser.print_help() 28 | return 29 | 30 | candidates = [ os.path.join(site_config_dir('bitex'), 'api_receive.ini'), 31 | arguments.config ] 32 | config = configparser.SafeConfigParser() 33 | config.read( candidates ) 34 | 35 | # Validate the whole file 36 | for section_name in config.sections(): 37 | options = ProjectOptions(config, section_name) 38 | if not options.log or \ 39 | not options.port or \ 40 | not options.rpc_url or \ 41 | not options.db_engine: 42 | raise RuntimeError("Invalid configuration file") 43 | 44 | # Start all applications 45 | applications = [] 46 | for section_name in config.sections(): 47 | options = ProjectOptions(config, section_name) 48 | 49 | application = ApiReceiveApplication(options, section_name) 50 | application.listen(options.port) 51 | applications.append(application) 52 | 53 | if applications: 54 | # start 55 | try: 56 | tornado.ioloop.IOLoop.instance().start() 57 | except KeyboardInterrupt: 58 | for application in applications: 59 | application.clean_up() 60 | 61 | if __name__ == "__main__": 62 | main() 63 | -------------------------------------------------------------------------------- /receiver/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, DateTime, Text, Boolean 2 | from sqlalchemy.ext.declarative import declarative_base 3 | 4 | import datetime 5 | 6 | 7 | Base = declarative_base() 8 | 9 | 10 | class ForwardingAddress(Base): 11 | __tablename__ = 'forwarding_address' 12 | id = Column(Integer, primary_key=True) 13 | callback = Column(String(255), nullable=False) 14 | destination_address = Column(String(40), nullable=False ) 15 | input_address = Column(String(40), nullable=False, index=True) 16 | input_transaction_hash = Column(String(255)) 17 | transaction_hash = Column(String(255)) 18 | payee_addresses = Column(Text) 19 | confirmations = Column(Integer,nullable=False, default=0) 20 | value = Column(Integer) 21 | fwd_miners_fee = Column(Integer) 22 | input_miners_fee = Column(Integer) 23 | status = Column(Integer,nullable=False, default=0, index=True) 24 | signed_fwd_transaction = Column(Text) 25 | created = Column(DateTime, default=datetime.datetime.now, nullable=False, index=True) 26 | transmitted = Column(DateTime) 27 | is_confirmed_by_client = Column(Boolean, nullable=False, default=False, index=True) 28 | confirm_callback_attempt = Column(Integer,nullable=False, default=0) 29 | callback_number_of_errors = Column(Integer,nullable=False, default=0) 30 | 31 | def __repr__(self): 32 | return ""\ 35 | % (self.id, self.callback, self.destination_address, self.input_address, self.input_transaction_hash, 36 | self.transaction_hash, self.confirmations, self.value, self.fwd_miners_fee, self.input_miners_fee, self.status, 37 | self.is_confirmed_by_client, self.signed_fwd_transaction, self.payee_addresses) 38 | 39 | 40 | @staticmethod 41 | def create( session, destination_address, input_address, callback): 42 | rec = ForwardingAddress(callback = callback,destination_address = destination_address,input_address=input_address) 43 | session.add(rec) 44 | session.commit() 45 | return rec 46 | 47 | @staticmethod 48 | def get_by_id(session, id): 49 | return session.query(ForwardingAddress).filter_by(id=id).first() 50 | 51 | 52 | @staticmethod 53 | def get_by_input_address(session, input_address): 54 | return session.query(ForwardingAddress).filter_by(input_address=input_address).first() 55 | 56 | @staticmethod 57 | def get_unconfirmed_by_client(session): 58 | return session.query(ForwardingAddress).filter_by(is_confirmed_by_client=False).filter(ForwardingAddress.status > 0) 59 | 60 | def get_callback_url(self): 61 | from urllib.parse import urlparse, ParseResult 62 | import urllib.request, urllib.parse, urllib.error 63 | 64 | callback_url_parse = urlparse(self.callback) 65 | query_args = { 66 | 'fwd_fee' : self.fwd_miners_fee or 0, 67 | 'input_fee' : self.input_miners_fee or 0, 68 | 'value' : self.value, 69 | 'input_address' : self.input_address, 70 | 'confirmations' : self.confirmations, 71 | 'transaction_hash' : self.transaction_hash, 72 | 'input_transaction_hash': self.input_transaction_hash, 73 | 'destination_address' : self.destination_address, 74 | 'payee_addresses' : self.payee_addresses 75 | } 76 | 77 | if not callback_url_parse.query: 78 | url_query = urllib.parse.urlencode(query_args) 79 | else: 80 | url_query = callback_url_parse.query + '&' + urllib.parse.urlencode(query_args) 81 | 82 | callback_url = ParseResult(scheme=callback_url_parse.scheme , 83 | netloc=callback_url_parse.netloc, 84 | path=callback_url_parse.path, 85 | params=callback_url_parse.params, 86 | query= url_query, 87 | fragment=callback_url_parse.fragment).geturl() 88 | 89 | return callback_url 90 | 91 | def set_as_completed(self, input_transaction_hash,transaction_hash,value,fwd_miners_fee,input_miners_fee,signed_fwd_transaction, payee_addresses = None): 92 | self.input_transaction_hash = input_transaction_hash 93 | self.transaction_hash = transaction_hash 94 | self.value = value 95 | self.fwd_miners_fee = fwd_miners_fee 96 | self.input_miners_fee = input_miners_fee 97 | self.signed_fwd_transaction = signed_fwd_transaction 98 | self.payee_addresses = payee_addresses 99 | self.status = 1 100 | 101 | 102 | def set_as_transmitted(self, transaction_hash=None): 103 | if transaction_hash: 104 | self.transaction_hash = transaction_hash 105 | self.transmitted = datetime.datetime.now() 106 | self.status = 2 107 | 108 | def is_complete(self): 109 | return self.status >= 1 110 | 111 | 112 | def is_transmitted(self): 113 | return self.status >= 2 114 | 115 | 116 | def db_bootstrap(session): 117 | pass 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /receiver/wallet_notify_handler.py: -------------------------------------------------------------------------------- 1 | import tornado.web 2 | import tornado.httpclient 3 | import decimal 4 | import json 5 | 6 | class WalletNotifyHandler(tornado.web.RequestHandler): 7 | def get(self, txid): 8 | self.application.log('HTTP_GET', '/api/walletnotify/' + txid ) 9 | 10 | miners_fee = self.application.paytxfee 11 | 12 | from models import ForwardingAddress 13 | transaction = self.application.bitcoind.gettransaction(txid) 14 | 15 | if transaction is None: 16 | self.send_error(404) 17 | return 18 | 19 | input_address = None 20 | for transaction_detail in transaction['details']: 21 | if transaction_detail['category'] == 'receive': 22 | input_address = transaction_detail['address'] 23 | 24 | if input_address is None: 25 | self.application.log('ERROR', 'Input address not found' ) 26 | self.send_error(404) 27 | return 28 | 29 | fwd_transaction_record = ForwardingAddress.get_by_input_address(self.application.db_session, input_address) 30 | if fwd_transaction_record is None: 31 | self.application.log('ERROR', 'Did not a forwarded transaction for ' + input_address ) 32 | self.send_error(404) 33 | return 34 | 35 | if fwd_transaction_record.is_transmitted() and fwd_transaction_record.input_transaction_hash == txid: 36 | self.write('*ok*') 37 | return 38 | 39 | elif fwd_transaction_record.is_transmitted() and fwd_transaction_record.input_transaction_hash != txid: 40 | self.application.log('DEBUG', 'User is sending a second transaction to the same address' ) 41 | fwd_transaction_record = ForwardingAddress.create( self.application.db_session, 42 | fwd_transaction_record.destination_address, 43 | fwd_transaction_record.input_address, 44 | fwd_transaction_record.callback ) 45 | 46 | 47 | 48 | if not fwd_transaction_record.is_complete(): 49 | destination_address = fwd_transaction_record.destination_address 50 | 51 | raw_transaction = self.application.bitcoind.getrawtransaction(txid) 52 | decoded_raw_transaction = self.application.bitcoind.decoderawtransaction(raw_transaction) 53 | 54 | # get the the payee addresses 55 | payee_addresses = [] 56 | total_input_value = decimal.Decimal(0) 57 | try: 58 | for input in decoded_raw_transaction['vin']: 59 | input_raw_tx = self.application.bitcoind.getrawtransaction(input['txid']) 60 | decoded_input_raw_tx = self.application.bitcoind.decoderawtransaction(input_raw_tx) 61 | total_input_value += decoded_input_raw_tx['vout'][ input['vout'] ]['value'] 62 | for payee_address in decoded_input_raw_tx['vout'][ input['vout'] ]['scriptPubKey']['addresses']: 63 | if payee_address not in payee_addresses: 64 | payee_addresses.append(payee_address) 65 | except Exception: 66 | pass 67 | 68 | total_output_value = decimal.Decimal(0) 69 | for vout in decoded_raw_transaction['vout']: 70 | total_output_value += vout['value'] 71 | 72 | vout_index = 0 73 | found_address = False 74 | for vout in decoded_raw_transaction['vout']: 75 | found_address = False 76 | for vout_address in vout['scriptPubKey']['addresses']: 77 | if input_address == vout_address: 78 | found_address = True 79 | break 80 | if found_address: 81 | break 82 | vout_index += 1 83 | 84 | if not found_address: 85 | self.send_error() 86 | return 87 | 88 | vout = decoded_raw_transaction['vout'][vout_index] 89 | input_value = vout['value'] 90 | fwd_value = vout['value'] - miners_fee 91 | input_miners_fee = total_input_value - total_output_value 92 | 93 | self.application.log('DEBUG', 'destination_address='+destination_address + 94 | ', input_miners_fee='+str(input_miners_fee) + 95 | ', payee_addresses='+ str(payee_addresses)) 96 | 97 | try: 98 | fwd_raw_transaction = self.application.bitcoind.createrawtransaction( 99 | [{"txid" : txid, "vout" : vout_index}], 100 | { destination_address: float(fwd_value) } 101 | ) 102 | except Exception as e: 103 | print((str(e))) 104 | raise 105 | 106 | signed_fwd_raw_transaction = self.application.bitcoind.signrawtransaction (fwd_raw_transaction,[{ 107 | "txid" : txid, 108 | "vout" : vout_index, 109 | "scriptPubKey" : vout['scriptPubKey']['hex'] 110 | }]) 111 | 112 | decoded_signed_fwd_raw_transaction = self.application.bitcoind.decoderawtransaction(signed_fwd_raw_transaction['hex']) 113 | transaction_hash = decoded_signed_fwd_raw_transaction['txid'] 114 | 115 | self.application.log('DEBUG', 'transaction_hash='+ transaction_hash + ', fwd_miners_fee='+str(miners_fee)) 116 | 117 | fwd_transaction_record.set_as_completed(txid, 118 | transaction_hash, 119 | int(float(input_value) * 1e8), 120 | int(float(miners_fee) * 1e8), 121 | int(float(input_miners_fee) * 1e8), 122 | signed_fwd_raw_transaction['hex'], 123 | json.dumps(payee_addresses) ) 124 | self.application.db_session.add(fwd_transaction_record) 125 | self.application.db_session.commit() 126 | 127 | self.application.invoke_callback_url(fwd_transaction_record) 128 | 129 | transaction_hash = self.application.bitcoind.sendrawtransaction(fwd_transaction_record.signed_fwd_transaction) 130 | 131 | fwd_transaction_record.set_as_transmitted(transaction_hash) 132 | self.application.db_session.add(fwd_transaction_record) 133 | self.application.db_session.commit() 134 | 135 | 136 | self.write('*ok*') 137 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.3 2 | backports-abc==0.5 3 | certifi==2017.4.17 4 | packaging==16.8 5 | pycountry==17.1.8 6 | pyotp==2.2.4 7 | pyparsing==2.2.0 8 | pyzmq==16.0.2 9 | singledispatch==3.4.0.3 10 | six==1.10.0 11 | SQLAlchemy==1.1.9 12 | tornado==4.5.1 13 | zmq==0.0.0 14 | -------------------------------------------------------------------------------- /ssl/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | RUN apk --update add bash openssl && mkdir -p /opt/bitex/ssl 4 | 5 | WORKDIR /opt/bitex/ssl 6 | 7 | COPY ssl/generate-server-certs /usr/local/bin/generate-server-certs 8 | 9 | RUN chmod +x /usr/local/bin/generate-server-certs 10 | 11 | CMD /usr/local/bin/generate-server-certs 12 | 13 | -------------------------------------------------------------------------------- /ssl/generate-server-certs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /opt/bitex/ssl 4 | openssl genrsa -des3 -passout pass:1234 -out server.pass.key 1024 5 | openssl rsa -passin pass:1234 -in server.pass.key -out server.key 6 | rm server.pass.key 7 | openssl req -new -key server.key -out server.csr \ 8 | -subj "/C=BR/ST=Sao Paulo/L=Sao Paulo/O=Bitex/OU=devel/CN=bitex" 9 | openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt 10 | -------------------------------------------------------------------------------- /ws_gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7 2 | ADD requirements.txt /tmp/requirements.txt 3 | RUN pip install -r /tmp/requirements.txt 4 | ADD ws_gateway /code 5 | ADD bitex.ini /code 6 | ADD libraries /libraries 7 | WORKDIR /code 8 | ENTRYPOINT ["python"] 9 | CMD [ "main.py", "-i", "ws_gateway_8445_demo"] 10 | -------------------------------------------------------------------------------- /ws_gateway/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitex-coin/backend/7296fb74988e9c7d64db8fcdd0eeb69f15b041ec/ws_gateway/__init__.py -------------------------------------------------------------------------------- /ws_gateway/deposit_hander.py: -------------------------------------------------------------------------------- 1 | from StringIO import StringIO 2 | 3 | import tornado.ioloop 4 | import tornado.web 5 | import tornado.httpclient 6 | import datetime 7 | import json 8 | 9 | def convertCamelCase2Underscore(name): 10 | import re 11 | s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) 12 | return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() 13 | 14 | def generate_template(template, params, key=None ): 15 | t = template 16 | for k, v in params.iteritems(): 17 | if isinstance(v, dict): 18 | if key: 19 | t = generate_template(t, v, key + ':' + convertCamelCase2Underscore(k) ) 20 | else: 21 | t = generate_template(t, v, convertCamelCase2Underscore(k) ) 22 | else: 23 | if key: 24 | t = t.replace('*|' + key + ':' + k + '|*', str(v) ) 25 | t = t.replace('*|' + key + ':' + k.upper() + '|*', str(v) ) 26 | t = t.replace('*|' + key + ':' + k.lower() + '|*', str(v) ) 27 | t = t.replace('*|' + key + ':' + convertCamelCase2Underscore(k) + '|*', str(v) ) 28 | t = t.replace('*|' + key + ':' + convertCamelCase2Underscore(k).upper() + '|*', str(v) ) 29 | else: 30 | t = t.replace('*|' + k + '|*', str(v) ) 31 | t = t.replace('*|' + k.upper() + '|*', str(v) ) 32 | t = t.replace('*|' + k.lower() + '|*', str(v) ) 33 | t = t.replace('*|' + convertCamelCase2Underscore(k) + '|*', str(v) ) 34 | t = t.replace('*|' + convertCamelCase2Underscore(k).upper() + '|*', str(v) ) 35 | return t 36 | 37 | class DepositHandler(tornado.web.RequestHandler): 38 | def __init__(self, application, request, **kwargs): 39 | super(DepositHandler, self).__init__(application, request, **kwargs) 40 | self.remote_ip = request.headers.get('X-Forwarded-For', request.headers.get('X-Real-Ip', request.remote_ip)) 41 | 42 | def write_deposit_template(self, deposit_method, deposit): 43 | raw_html_template = deposit_method.get('HtmlTemplate') 44 | deposit_data = deposit.toJSON() 45 | deposit_data['Value'] = self.application.format_currency(deposit_data['Currency'],deposit_data['Value']) 46 | deposit_html = generate_template(raw_html_template, deposit_data) 47 | self.write(deposit_html) 48 | 49 | def write_brazilian_deposit_system(self,deposit): 50 | deposit_data = deposit.get('Data') 51 | if 'data_documento' in deposit_data and deposit_data['data_documento']: 52 | deposit_data['data_documento'] = datetime.datetime.strptime( deposit_data['data_documento'] , "%Y-%m-%d").date() 53 | 54 | if 'data_vencimento' in deposit_data and deposit_data['data_vencimento']: 55 | deposit_data['data_vencimento'] = datetime.datetime.strptime( deposit_data['data_vencimento'] , "%Y-%m-%d").date() 56 | 57 | if 'data_processamento' in deposit_data and deposit_data['data_processamento']: 58 | deposit_data['data_processamento'] = datetime.datetime.strptime( deposit_data['data_processamento'] , "%Y-%m-%d").date() 59 | 60 | buffer = StringIO() 61 | from pyboleto.pdf import BoletoPDF 62 | boleto_pdf = BoletoPDF(buffer) 63 | 64 | from pyboleto import bank 65 | ClasseBanco = bank.get_class_for_codigo(deposit_data['codigo_banco']) 66 | deposit_dados = ClasseBanco() 67 | for field_name, field_value in deposit_data.iteritems(): 68 | if field_value: 69 | setattr(deposit_dados, field_name, field_value) 70 | boleto_pdf.drawBoleto(deposit_dados) 71 | 72 | 73 | self.set_header("Content-Type", "application/pdf") 74 | 75 | 76 | boleto_pdf.save() 77 | pdf_file = buffer.getvalue() 78 | 79 | self.write( pdf_file ) 80 | 81 | 82 | 83 | def get(self, *args, **kwargs): 84 | deposit_id = self.get_argument("deposit_id", default=None, strip=False) 85 | download = int(self.get_argument("download", default="0", strip=False)) 86 | if not deposit_id: 87 | self.send_error(404) 88 | return 89 | 90 | deposit_response_msg = self.application.application_trade_client.sendString( 91 | json.dumps({ 'MsgType': 'U18', 'DepositReqID': 1, 'DepositID': deposit_id })) 92 | 93 | if not deposit_response_msg or not deposit_response_msg.isDepositResponse(): 94 | self.send_error(404) 95 | return 96 | 97 | deposit_method_id = deposit_response_msg.get('DepositMethodID') 98 | deposit_method_response_msg = self.application.application_trade_client.sendString( 99 | json.dumps({ 'MsgType': 'U48', 'DepositMethodReqID': 1, 'DepositMethodID': deposit_method_id })) 100 | 101 | if not deposit_method_response_msg or not deposit_method_response_msg.isDepositMethodResponse(): 102 | self.send_error(404) 103 | return 104 | 105 | if not isinstance(deposit_response_msg.get('Data'), dict): 106 | self.send_error() 107 | return 108 | 109 | deposit_response_msg.get('Data')['remote_ip'] = self.remote_ip 110 | 111 | if download == 1: 112 | self.set_header("Content-Disposition", 113 | "attachment; filename=%s"%deposit_method_response_msg['DepositMethodName'] + '.html') 114 | 115 | if deposit_response_msg.get('Type') == 'BBS': 116 | self.write_brazilian_deposit_system(deposit_response_msg) 117 | elif deposit_response_msg.get('Type') == 'DTP': 118 | self.write_deposit_template(deposit_method_response_msg, deposit_response_msg) 119 | 120 | else: 121 | self.write('Invalid deposit type') 122 | -------------------------------------------------------------------------------- /ws_gateway/deposit_receipt_webhook_handler.py: -------------------------------------------------------------------------------- 1 | import tornado.ioloop 2 | import tornado.web 3 | import tornado.httpclient 4 | import datetime 5 | import json 6 | 7 | class DepositReceiptWebHookHandler(tornado.web.RequestHandler): 8 | def __init__(self, application, request, **kwargs): 9 | super(DepositReceiptWebHookHandler, self).__init__(application, request, **kwargs) 10 | 11 | def post(self, *args, **kwargs): 12 | submissionID = self.get_argument('submissionID') 13 | 14 | raw_request = json.loads(self.get_argument('rawRequest')) 15 | deposit_id = None 16 | deposit_receipt = None 17 | for key, value in raw_request.iteritems(): 18 | if 'deposit_id' in key: 19 | deposit_id = value 20 | elif 'depositReceipt' in key: 21 | deposit_receipt = value 22 | 23 | import random 24 | req_id = random.randrange(600000,900000) 25 | 26 | message = { 27 | 'MsgType': 'B0', 28 | 'ProcessDepositReqID':req_id, 29 | 'Action': 'CONFIRM', 30 | 'DepositID': deposit_id, 31 | 'Data': { 32 | 'DepositReceipt': deposit_receipt, 33 | 'SubmissionID': submissionID 34 | } 35 | } 36 | 37 | self.application.application_trade_client.sendJSON(message) 38 | self.write('*ok*') 39 | -------------------------------------------------------------------------------- /ws_gateway/instrument_helper.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from signals import Signal 3 | 4 | import time 5 | 6 | signal_publish_security_status = Signal() 7 | 8 | class InstrumentStatusHelper(object): 9 | """" InstrumentStatusHelper. """ 10 | 11 | def __init__(self, symbol = "ALL"): 12 | self.symbol = str(symbol) 13 | self.last_trades = [] 14 | self.volume_price = 1 15 | self.volume_size = 1 16 | self.last_price = 1 17 | self.max_price = 1 18 | self.min_price = 1 19 | self.bid = 1 20 | self.ask = 1 21 | self.timestamp_last_update = int(time.time() * 1000) 22 | 23 | def set_best_bid(self, bid ): 24 | if self.bid != bid: 25 | self.timestamp_last_update = int(time.time() * 1000) 26 | self.bid = bid 27 | signal_publish_security_status('SECURITY_STATUS', self) 28 | 29 | def set_best_ask(self, ask ): 30 | if self.ask != ask: 31 | self.timestamp_last_update = int(time.time() * 1000) 32 | self.ask = ask 33 | signal_publish_security_status('SECURITY_STATUS', self) 34 | 35 | def _subtract_trade(self, trade): 36 | volume_price = int( 37 | trade['price'] * 38 | trade['size'] / 39 | 1.e8) 40 | 41 | self.volume_price -= volume_price 42 | self.volume_size -= trade['size'] 43 | 44 | if self.max_price == trade['price']: 45 | if self.last_trades: 46 | max_price_record = max(self.last_trades, key=lambda x:x['price']) 47 | if max_price_record: 48 | self.max_price = max_price_record['price'] 49 | else: 50 | self.max_price = 0 51 | else: 52 | self.max_price = 0 53 | 54 | if self.min_price == trade['price']: 55 | if self.last_trades: 56 | min_price = min(self.last_trades, key=lambda x:x['price']) 57 | if min_price: 58 | self.min_price = min_price['price'] 59 | 60 | def _add_trade(self, trade): 61 | volume_price = int( trade['price'] * trade['size'] / 1.e8) 62 | self.volume_price += volume_price 63 | self.volume_size += trade['size'] 64 | 65 | if trade['price'] > self.max_price: 66 | self.max_price = trade['price'] 67 | 68 | if self.min_price is None or trade['price'] < self.min_price: 69 | self.min_price = trade['price'] 70 | 71 | def push_trade(self, trade): 72 | while True: 73 | if len(self.last_trades) > 0: 74 | d1 = datetime.strptime(self.last_trades[-1]['trade_date'] + ' ' + self.last_trades[-1]['trade_time'], '%Y-%m-%d %H:%M:%S') 75 | d2 = datetime.strptime(trade['trade_date'] + ' ' + trade['trade_time'], '%Y-%m-%d %H:%M:%S') 76 | if (d2 - d1).days > 0: 77 | removed_trade = self.last_trades.pop() 78 | self._subtract_trade(removed_trade) 79 | continue 80 | break 81 | 82 | self.last_trades.insert(0,trade) 83 | self._add_trade(trade) 84 | self.last_price = trade['price'] 85 | 86 | signal_publish_security_status('SECURITY_STATUS', self) 87 | -------------------------------------------------------------------------------- /ws_gateway/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from datetime import datetime, timedelta 5 | 6 | from sqlalchemy import create_engine, func 7 | from sqlalchemy.sql.expression import or_, and_ 8 | from sqlalchemy import Column, Integer, String, DateTime 9 | from sqlalchemy.ext.declarative import declarative_base 10 | from sqlalchemy.orm import scoped_session, sessionmaker 11 | 12 | Base = declarative_base() 13 | 14 | class Trade(Base): 15 | __tablename__ = 'trade' 16 | id = Column(Integer, primary_key=True) 17 | order_id = Column(Integer, nullable=False) 18 | counter_order_id = Column(Integer, nullable=False) 19 | buyer_id = Column(Integer, nullable=False) 20 | seller_id = Column(Integer, nullable=False) 21 | buyer_username = Column(String(15), nullable=False) 22 | seller_username = Column(String(15), nullable=False) 23 | side = Column(String(1), nullable=False) 24 | symbol = Column(String(12), nullable=False, index=True) 25 | size = Column(Integer, nullable=False) 26 | price = Column(Integer, nullable=False) 27 | created = Column(DateTime, nullable=False, index=True) 28 | trade_type = Column(Integer, nullable=False, default=0) # regular trade 29 | 30 | def __repr__(self): 31 | return ""\ 33 | % (self.id, self.order_id, self.counter_order_id, self.buyer_id, self.seller_id, self.buyer_username, self.seller_username, 34 | self.side, self.symbol, self.size, self.price, self.created, self.trade_type) 35 | 36 | @staticmethod 37 | def get_trade(session, trade_id=None): 38 | if trade_id: 39 | filter_obj = or_(Trade.id == trade_id) 40 | else: 41 | return None 42 | trade = session.query(Trade).filter(filter_obj).first() 43 | if trade: 44 | return trade 45 | return None 46 | 47 | @staticmethod 48 | def get_all_trades(session): 49 | return session.query(Trade) 50 | 51 | @staticmethod 52 | def get_trades(session, symbol, since): 53 | if since > 1000000000: 54 | since_timestamp = datetime.utcfromtimestamp(since) 55 | trades = session.query(Trade).filter( 56 | Trade.created >= since_timestamp).filter(Trade.symbol == symbol).order_by(Trade.id.desc()) 57 | else: 58 | trades = session.query(Trade).filter( 59 | Trade.id > int(since)).filter(Trade.symbol == symbol).order_by(Trade.id.desc()) 60 | 61 | return trades 62 | 63 | @staticmethod 64 | def get_last_trade_id(session): 65 | res = session.query(func.max(Trade.id)).one() 66 | return res[0] 67 | 68 | @staticmethod 69 | def get_last_trades(session, since = None, page_size = None, offset = None, sort_column = None, sort_order='ASC'): 70 | if since is not None: 71 | if since > 1000000000: 72 | since = datetime.utcfromtimestamp(since) 73 | filter_obj = and_(Trade.created >= since) 74 | else: 75 | filter_obj = and_(Trade.id > int(since)) 76 | else: 77 | today = datetime.now() 78 | since = today - timedelta(days=1) 79 | filter_obj = and_(Trade.created >= since) 80 | 81 | trades = session.query(Trade).filter(filter_obj).order_by( Trade.id.desc()) 82 | 83 | if page_size: 84 | trades = trades.limit(page_size) 85 | if offset: 86 | trades = trades.offset(offset) 87 | if sort_column: 88 | if sort_order == 'ASC': 89 | trades = trades.order(sort_column) 90 | else: 91 | trades = trades.order(sort_column).desc() 92 | 93 | return trades 94 | 95 | @staticmethod 96 | def create(session, msg): 97 | trade = Trade.get_trade(session, msg['id']) 98 | if not trade: 99 | trade = Trade(id=msg['id'], 100 | order_id=msg['order_id'], 101 | counter_order_id=msg['counter_order_id'], 102 | buyer_id=msg['buyer_id'], 103 | seller_id=msg['seller_id'], 104 | buyer_username=msg['buyer_username'], 105 | seller_username=msg['seller_username'], 106 | side=msg['side'], 107 | symbol=msg['symbol'], 108 | size=msg['size'], 109 | price=msg['price'], 110 | created=datetime.strptime(msg['trade_date'] + ' ' + msg['trade_time'], "%Y-%m-%d %H:%M:%S")) 111 | 112 | session.add(trade) 113 | session.commit() 114 | 115 | return trade 116 | 117 | 118 | 119 | 120 | def db_bootstrap(session): 121 | pass 122 | -------------------------------------------------------------------------------- /ws_gateway/process_deposit_handler.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.insert(0, os.path.join( os.path.dirname(__file__), '../' ) ) 4 | 5 | import tornado.ioloop 6 | import tornado.web 7 | import tornado.httpclient 8 | import datetime 9 | import json 10 | 11 | from zmq_client import TradeClientException 12 | 13 | class ProcessDepositHandler(tornado.web.RequestHandler): 14 | def __init__(self, application, request, **kwargs): 15 | super(ProcessDepositHandler, self).__init__(application, request, **kwargs) 16 | self.remote_ip = request.headers.get('X-Forwarded-For', request.headers.get('X-Real-Ip', request.remote_ip)) 17 | 18 | def get(self, *args, **kwargs): 19 | secret = self.get_argument("s", default=None, strip=False) 20 | if not secret: 21 | raise tornado.httpclient.HTTPError( 404 ) 22 | 23 | fwd_fee = int(self.get_argument("fwd_fee", default=0, strip=False)) 24 | input_fee = int(self.get_argument("input_fee", default=0, strip=False)) 25 | value = int(self.get_argument("value", default=0, strip=False)) 26 | input_address = self.get_argument("input_address", default=None, strip=False) 27 | input_transaction_hash = self.get_argument("input_transaction_hash", default=None, strip=False) 28 | transaction_hash = self.get_argument("transaction_hash", default=None, strip=False) 29 | confirmations = self.get_argument("confirmations", default=0, strip=False) 30 | payee_addresses = self.get_argument("payee_addresses", default=None, strip=False) 31 | 32 | import random 33 | req_id = random.randrange(600000,900000) 34 | 35 | payee_addresses_json = None 36 | if payee_addresses: 37 | try: 38 | payee_addresses_json = json.loads(payee_addresses) 39 | except Exception,e: 40 | pass 41 | 42 | 43 | 44 | process_deposit_message = { 45 | 'MsgType': 'B0', 46 | 'ProcessDepositReqID':req_id, 47 | 'Action': 'COMPLETE', 48 | 'Secret': secret, 49 | 'Amount': value, 50 | 'Data': { 51 | 'Confirmations': int(confirmations), 52 | 'InputAddress': input_address, 53 | 'InputTransactionHash': input_transaction_hash, 54 | 'TransactionHash': transaction_hash, 55 | } 56 | } 57 | 58 | if fwd_fee: 59 | process_deposit_message['Data']['ForwardFee'] = fwd_fee 60 | 61 | if input_fee: 62 | process_deposit_message['Data']['InputFee'] = input_fee 63 | 64 | if payee_addresses_json: 65 | process_deposit_message['Data']['PayeeAddresses'] = json.dumps(payee_addresses_json) 66 | 67 | try: 68 | response_msg = self.application.application_trade_client.sendJSON(process_deposit_message) 69 | except TradeClientException, e: 70 | self.write_error(400) 71 | return 72 | 73 | if response_msg.get('Status') == '4': 74 | self.write("*ok*") 75 | self.write("") 76 | -------------------------------------------------------------------------------- /ws_gateway/rest_api_handler.py: -------------------------------------------------------------------------------- 1 | import tornado.web 2 | import tornado.httpclient 3 | import calendar 4 | import json 5 | 6 | from market_data_helper import MarketDataSubscriber 7 | 8 | class RestApiHandler(tornado.web.RequestHandler): 9 | def head(self, version, symbol, resource): 10 | self._process_request(version, symbol, resource) 11 | 12 | def get(self, version, symbol, resource): 13 | self._process_request(version, symbol, resource) 14 | 15 | def _send_tiker(self, symbol): 16 | md_subscriber = MarketDataSubscriber.get(symbol, self.application) 17 | 18 | ticker = { 19 | "pair": symbol, 20 | "high": md_subscriber.inst_status.max_price / 1e8, 21 | "low": md_subscriber.inst_status.min_price / 1e8, 22 | "last": md_subscriber.inst_status.last_price / 1e8, 23 | "vol_" + symbol[3:].lower(): md_subscriber.inst_status.volume_price / 1e8, 24 | "vol": md_subscriber.inst_status.volume_size / 1e8, 25 | "buy": md_subscriber.inst_status.bid / 1e8, 26 | "sell": md_subscriber.inst_status.ask / 1e8 27 | } 28 | self.write( json.dumps(ticker)) 29 | 30 | def _send_order_book(self, symbol): 31 | md_subscriber = MarketDataSubscriber.get(symbol, self.application.db_session) 32 | 33 | bids = [] 34 | asks = [] 35 | 36 | for order in md_subscriber.buy_side: 37 | bids.append([order['price']/1e8, order['qty']/1e8, order['user_id']]) 38 | 39 | for order in md_subscriber.sell_side: 40 | asks.append([order['price']/1e8, order['qty']/1e8, order['user_id']]) 41 | 42 | self.write( 43 | { 44 | 'pair': symbol, 45 | 'bids': bids, 46 | 'asks': asks 47 | } 48 | ) 49 | 50 | def _send_trades(self, symbol, since): 51 | md_subscriber = MarketDataSubscriber.get(symbol, self.application) 52 | trades = [] 53 | 54 | for trade in md_subscriber.get_trades(symbol, since): 55 | trades.append({ 56 | 'tid': trade.id, 57 | 'price': trade.price/1e8, 58 | 'amount': trade.size/1e8, 59 | 'date': calendar.timegm(trade.created.timetuple()), 60 | }) 61 | 62 | self.write(json.dumps(trades)) 63 | 64 | def _process_request(self, version, symbol, resource): 65 | currency = self.get_argument("crypto_currency", default='BTC', strip=False) 66 | since = self.get_argument("since", default=0, strip=False) 67 | callback = self.get_argument("callback", default='', strip=False) 68 | if not callback: 69 | callback = self.get_argument("jsonp", default='', strip=False) 70 | 71 | instrument = '%s%s'%(currency, symbol) 72 | 73 | if callback: 74 | self.write( callback + '(' ) 75 | if version == 'v1': 76 | if resource == 'orderbook': 77 | self._send_order_book(instrument) 78 | elif resource == 'trades': 79 | self._send_trades(instrument, float(since)) 80 | elif resource == 'ticker': 81 | self._send_tiker(instrument) 82 | else: 83 | self.send_error(404) 84 | else: 85 | self.send_error(404) 86 | 87 | if callback: 88 | self.write( ');' ) 89 | -------------------------------------------------------------------------------- /ws_gateway/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from test_signals import * -------------------------------------------------------------------------------- /ws_gateway/tests/test_signals.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | ROOT_PATH = os.path.abspath( os.path.join(os.path.dirname(__file__), "../")) 6 | sys.path.insert( 0, ROOT_PATH) 7 | 8 | import unittest 9 | import mock 10 | 11 | from market_data_helper import MarketDataPublisher, MarketDataSubscriber, signal_publish_md_status, signal_publish_md_order_depth_incremental 12 | 13 | class MarketDataPubSubTest(unittest.TestCase): 14 | def setUp(self): 15 | self.application = mock.Mock() 16 | self.application.db_session = mock.Mock() 17 | 18 | self.md_subscriber = {} 19 | self.md_subscriber["BTCUSD"] = MarketDataSubscriber.get("BTCUSD", self.application) 20 | self.md_subscriptions = {} 21 | 22 | self.zmq_context = mock.Mock() 23 | self.zmq_context.socket = mock.MagicMock() 24 | self.zmq_context.connect = mock.MagicMock() 25 | self.zmq_context.setsockopt = mock.MagicMock() 26 | 27 | self.application_trade_client = mock.Mock() 28 | 29 | self.trade_pub = "XXX" 30 | 31 | self.on_send_json_msg_to_user = mock.Mock(name='send_json_msg_to_user', return_value=None) 32 | 33 | @mock.patch('zmq.eventloop.zmqstream.ZMQStream') 34 | def testSubscribe(self, ZMQStreamMock): 35 | self.md_subscriber["BTCUSD"].subscribe( 36 | self.zmq_context, 37 | self.trade_pub, 38 | self.application_trade_client) 39 | 40 | self.application_trade_client.sendJSON.assert_called_with({ 41 | 'MDEntryTypes': ['0', '1', '2'], 42 | 'Instruments': ['BTCUSD'], 43 | 'MsgType': 'V', 44 | 'TradeDate': '20150115', 45 | 'MDReqID': '0', 46 | 'MDUpdateType': '0', 47 | 'SubscriptionRequestType': '0', 48 | 'MarketDepth': 0 49 | }) 50 | 51 | 52 | md_full_refresh_msg = {"MDReqID": "0", "Symbol": "BTCUSD", "MsgType": "W", "MDFullGrp": [], "MarketDepth": 0} 53 | self.md_subscriber["BTCUSD"].on_md_full_refresh(md_full_refresh_msg) 54 | 55 | 56 | self.md_subscriptions["0"] = [] 57 | self.md_subscriptions["0"].append( 58 | MarketDataPublisher( 59 | "0", 60 | 0, 61 | ["0","1"], 62 | "BTCUSD", 63 | self.on_send_json_msg_to_user, 64 | False)) 65 | print len(signal_publish_md_order_depth_incremental._methods_subs['BTCUSD.3'].items()) 66 | 67 | md_incrementa_msg = [] 68 | md_incrementa_msg.append({ 69 | "MsgType": "X", 70 | "MDBkTyp": "3", 71 | "MDIncGrp": [{ 72 | "OrderID": 1, 73 | "MDEntryPx": 40000000000, 74 | "UserID": 90000002, 75 | "MDEntryPositionNo": 1, 76 | "Username": "user", 77 | "MDUpdateAction": "0", 78 | "MDEntryTime": "22:08:14", 79 | "Symbol": "BTCUSD", 80 | "Broker": "exchange", 81 | "MDEntryType": "1", 82 | "MDEntrySize": 100000000, 83 | "MDEntryID": 1, 84 | "MDEntryDate": "2015-01-15" 85 | }] 86 | }) 87 | self.md_subscriber["BTCUSD"].on_md_incremental(md_incrementa_msg[0]) 88 | self.assertAlmostEquals( 1 ,self.on_send_json_msg_to_user.call_count) 89 | self.assertAlmostEquals( 0 , len(self.md_subscriber["BTCUSD"].buy_side) ) 90 | self.assertAlmostEquals( 1 , len(self.md_subscriber["BTCUSD"].sell_side) ) 91 | 92 | 93 | md_incrementa_msg.append({ 94 | "MsgType": "X", 95 | "MDBkTyp": "3", 96 | "MDIncGrp": [{ 97 | "OrderID": 2, 98 | "MDEntryPx": 40000000000, 99 | "UserID": 90000002, 100 | "MDEntryPositionNo": 2, 101 | "Username": "user", 102 | "MDUpdateAction": "0", 103 | "MDEntryTime": "22:10:28", 104 | "Symbol": "BTCUSD", 105 | "Broker": "exchange", 106 | "MDEntryType": "1", 107 | "MDEntrySize": 100000000, 108 | "MDEntryID": 2, 109 | "MDEntryDate": "2015-01-15" 110 | }] 111 | }) 112 | self.md_subscriber["BTCUSD"].on_md_incremental(md_incrementa_msg[1]) 113 | self.assertAlmostEquals( 2 ,self.on_send_json_msg_to_user.call_count) 114 | self.assertAlmostEquals( 0 , len(self.md_subscriber["BTCUSD"].buy_side) ) 115 | self.assertAlmostEquals( 2 , len(self.md_subscriber["BTCUSD"].sell_side) ) 116 | 117 | print len(signal_publish_md_order_depth_incremental._methods_subs['BTCUSD.3'].items()) 118 | 119 | # emulate 2k connections 120 | for x in xrange(1,2000): 121 | self.md_subscriptions[str(x)] = [] 122 | self.md_subscriptions[str(x)].append( 123 | MarketDataPublisher( 124 | "0", 125 | str(x), 126 | ["0","1"], 127 | "BTCUSD", 128 | self.on_send_json_msg_to_user, 129 | False)) 130 | 131 | md_incrementa_msg.append({ 132 | "MsgType": "X", 133 | "MDBkTyp": "3", 134 | "MDIncGrp": [{ 135 | "OrderID": 3, 136 | "MDEntryPx": 40000000000, 137 | "UserID": 90000002, 138 | "MDEntryPositionNo": 3, 139 | "Username": "user", 140 | "MDUpdateAction": "0", 141 | "MDEntryTime": "22:10:38", 142 | "Symbol": "BTCUSD", 143 | "Broker": "exchange", 144 | "MDEntryType": "1", 145 | "MDEntrySize": 100000000, 146 | "MDEntryID": 3, 147 | "MDEntryDate": "2015-01-15" 148 | }] 149 | }) 150 | self.on_send_json_msg_to_user.reset_mock() 151 | self.md_subscriber["BTCUSD"].on_md_incremental(md_incrementa_msg[2]) 152 | self.assertAlmostEquals( 0 , len(self.md_subscriber["BTCUSD"].buy_side) ) 153 | self.assertAlmostEquals( 3 , len(self.md_subscriber["BTCUSD"].sell_side) ) 154 | self.assertAlmostEquals( 2000 ,self.on_send_json_msg_to_user.call_count) 155 | print len(signal_publish_md_order_depth_incremental._methods_subs['BTCUSD.3'].items()) 156 | 157 | # let's close the first 1k connections 158 | for x in xrange(0,1000): 159 | del self.md_subscriptions[str(x)] 160 | 161 | md_incrementa_msg.append({ 162 | "MsgType": "X", 163 | "MDBkTyp": "3", 164 | "MDIncGrp": [{ 165 | "OrderID": 4, 166 | "MDEntryPx": 40000000000, 167 | "UserID": 90000002, 168 | "MDEntryPositionNo": 4, 169 | "Username": "user", 170 | "MDUpdateAction": "0", 171 | "MDEntryTime": "22:10:38", 172 | "Symbol": "BTCUSD", 173 | "Broker": "exchange", 174 | "MDEntryType": "1", 175 | "MDEntrySize": 100000000, 176 | "MDEntryID": 4, 177 | "MDEntryDate": "2015-01-15" 178 | }] 179 | }) 180 | self.on_send_json_msg_to_user.reset_mock() 181 | self.md_subscriber["BTCUSD"].on_md_incremental(md_incrementa_msg[2]) 182 | self.assertAlmostEquals( 0 , len(self.md_subscriber["BTCUSD"].buy_side) ) 183 | self.assertAlmostEquals( 4 , len(self.md_subscriber["BTCUSD"].sell_side) ) 184 | self.assertAlmostEquals( 1000 ,self.on_send_json_msg_to_user.call_count) 185 | 186 | print len(signal_publish_md_status._methods_subs['MD_STATUS'].items()) 187 | print len(signal_publish_md_order_depth_incremental._methods_subs['BTCUSD.3'].items()) -------------------------------------------------------------------------------- /ws_gateway/util.py: -------------------------------------------------------------------------------- 1 | __author__ = 'rodrigo' 2 | 3 | import pycountry 4 | 5 | def get_country_code(country_name): 6 | try: 7 | return pycountry.countries.get(name=country_name).alpha2 8 | except KeyError: 9 | jotform_countries_that_are_not_in_pycountry = { 10 | "The Bahamas" : "BS", 11 | "Bolivia" : 'BO', 12 | "Brunei": "BN", 13 | "People's Republic of China": "CN", 14 | "Republic of China": "CN", 15 | "Cote d'Ivoire": "CI", 16 | "Falkland Islands": "FK", 17 | "The Gambia": "GM", 18 | "Iran": "IR", 19 | "North Korea": "KP", 20 | "South Korea": "KR", 21 | "Kosovo": "XK", 22 | "Laos": "LA", 23 | "Macau": "MO", 24 | "Macedonia": "MK", 25 | "Micronesia": "FM", 26 | "Moldova": "MD", 27 | "Nagorno-Karabakh":"AZ", 28 | "Netherlands Antilles": "AN", 29 | "Turkish Republic of Northern Cyprus": "CY", 30 | "Northern Mariana":"MP", 31 | "Palestine": "PS", 32 | "Pitcairn Islands": "PN", 33 | "Russia":"RU", 34 | "Saint Barthelemy": "BL", 35 | "Saint Helena":"SH", 36 | "Saint Martin" : "MF", 37 | "Somaliland" : "SO", 38 | "South Ossetia": "GE", 39 | "Svalbard":"SJ", 40 | "Syria":"SY", 41 | "Taiwan":"TW", 42 | "Tanzania":"TZ", 43 | "Transnistria Pridnestrovie":"MD", 44 | "Tristan da Cunha":"SH", 45 | "Vatican City":"VA", 46 | "Venezuela":"VE", 47 | "Vietnam":"VN", 48 | "British Virgin Islands": "VG", 49 | "US Virgin Islands": "VI" 50 | } 51 | try: 52 | return jotform_countries_that_are_not_in_pycountry[country_name] 53 | except KeyError: 54 | return country_name 55 | 56 | 57 | -------------------------------------------------------------------------------- /ws_gateway/verification_webhook_handler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import tornado.ioloop 3 | import tornado.web 4 | import tornado.httpclient 5 | import datetime 6 | from time import mktime 7 | 8 | import json 9 | from util import get_country_code 10 | 11 | def camel_to_underscore(name): 12 | import re 13 | s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) 14 | return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() 15 | 16 | 17 | class VerificationWebHookHandler(tornado.web.RequestHandler): 18 | def __init__(self, application, request, **kwargs): 19 | super(VerificationWebHookHandler, self).__init__(application, request, **kwargs) 20 | 21 | def post(self, *args, **kwargs): 22 | formID = self.get_argument('formID') 23 | submissionID = self.get_argument('submissionID') 24 | 25 | dt = datetime.datetime.now() 26 | createdAt = int(mktime(dt.timetuple()) + dt.microsecond/1000000.0) 27 | 28 | raw_request = json.loads(self.get_argument('rawRequest')) 29 | print raw_request 30 | 31 | broker_id = None 32 | user_id = None 33 | first_name = None 34 | middle_name = None 35 | last_name = None 36 | birth_date_day = None 37 | birth_date_month = None 38 | birth_date_year = None 39 | phone_number_country = None 40 | phone_number_area = None 41 | phone_number_phone = None 42 | address_addr_line1 = None 43 | address_addr_line2 = None 44 | address_city = None 45 | address_state = None 46 | address_postal = None 47 | address_country = None 48 | address_country_code = None 49 | finger_print = None 50 | stunt_ip = None 51 | 52 | photo_fields = [] 53 | id_fields = [] 54 | 55 | for key, value in raw_request.iteritems(): 56 | if 'broker_id' in key: 57 | broker_id = int(value) 58 | if 'user_id' in key: 59 | user_id = int(value) 60 | if 'photo_fields' in key: 61 | photo_fields = value.split(',') 62 | if 'id_fields' in key: 63 | id_fields = value.split(',') 64 | 65 | # jotform 66 | if 'name' in key and isinstance(value, dict ) and 'first' in value: 67 | first_name = value['first'] 68 | if 'name' in key and isinstance(value, dict ) and 'middle' in value: 69 | middle_name = value['middle'] 70 | if 'name' in key and isinstance(value, dict ) and 'last' in value: 71 | last_name = value['last'] 72 | 73 | if 'birthDate' in key and isinstance(value, dict ) and 'day' in value: 74 | birth_date_day = value['day'] 75 | if 'birthDate' in key and isinstance(value, dict ) and 'day' in value: 76 | birth_date_month = value['month'] 77 | if 'birthDate' in key and isinstance(value, dict ) and 'day' in value: 78 | birth_date_year = value['year'] 79 | 80 | if 'phoneNumber' in key and isinstance(value, dict ) and 'country' in value: 81 | phone_number_country = value['country'] 82 | if 'phoneNumber' in key and isinstance(value, dict ) and 'area' in value: 83 | phone_number_area = value['area'] 84 | if 'phoneNumber' in key and isinstance(value, dict ) and 'phone' in value: 85 | phone_number_phone = value['phone'] 86 | 87 | if 'address' in key and isinstance(value, dict ) and 'addr_line1' in value: 88 | address_addr_line1 = value['addr_line1'] 89 | if 'address' in key and isinstance(value, dict ) and 'addr_line2' in value: 90 | address_addr_line2 = value['addr_line2'] 91 | if 'address' in key and isinstance(value, dict ) and 'city' in value: 92 | address_city = value['city'] 93 | if 'address' in key and isinstance(value, dict ) and 'state' in value: 94 | address_state = value['state'] 95 | if 'address' in key and isinstance(value, dict ) and 'postal' in value: 96 | address_postal = value['postal'] 97 | if 'address' in key and isinstance(value, dict ) and 'country' in value: 98 | address_country = value['country'] 99 | address_country_code = get_country_code(address_country) 100 | 101 | if 'finger_print' in key: 102 | finger_print = value 103 | 104 | if 'stunt_ip' in key: 105 | stunt_ip = value 106 | 107 | #form stack 108 | if 'name-first' in key: 109 | first_name = value 110 | if 'name-middle' in key: 111 | middle_name = value 112 | if 'name-last' in key: 113 | last_name = value 114 | 115 | if 'address-address' in key: 116 | address_addr_line1 = value 117 | if 'address-address2' in key: 118 | address_addr_line2 = value 119 | if 'address-city' in key: 120 | address_city = value 121 | if 'address-state' in key: 122 | address_state = value 123 | if 'address-zip' in key: 124 | address_postal = value 125 | if 'address-country' in key: 126 | address_country = value 127 | address_country_code = get_country_code(address_country) 128 | 129 | 130 | uploaded_files = [] 131 | for field in photo_fields: 132 | for key, value in raw_request.iteritems(): 133 | if field in key: 134 | if isinstance(value, list ): 135 | uploaded_files.extend(value) 136 | else: 137 | uploaded_files.append(value) 138 | 139 | 140 | import random 141 | req_id = random.randrange(600000,900000) 142 | 143 | if birth_date_month[:3].upper() in ['JAN', 'GEN']: 144 | birth_date_month = '01' 145 | elif birth_date_month[:3].upper() in ['FEV', 'FEB']: 146 | birth_date_month = '02' 147 | elif birth_date_month[:3].upper() in ['MAR', u'MÄR']: 148 | birth_date_month = '03' 149 | elif birth_date_month[:3].upper() in ['ABR', 'APR', 'AVR']: 150 | birth_date_month = '04' 151 | elif birth_date_month[:3].upper() in ['MAY', 'MAI', 'MAG']: 152 | birth_date_month = '05' 153 | elif birth_date_month[:3].upper() in ['JUN', 'GIU']: 154 | birth_date_month = '06' 155 | elif birth_date_month[:3] in ['jui']: 156 | birth_date_month = '06' 157 | elif birth_date_month[:3] in ['Jui']: 158 | birth_date_month = '07' 159 | elif birth_date_month[:3].upper() in ['JUL', 'LUG']: 160 | birth_date_month = '07' 161 | elif birth_date_month[:3].upper() in ['AGO', 'AUG', 'AOU']: 162 | birth_date_month = '08' 163 | elif birth_date_month[:3].upper() in ['SET', 'SEP']: 164 | birth_date_month = '09' 165 | elif birth_date_month[:3].upper() in ['OUT', 'OCT', 'OTT', 'OKT']: 166 | birth_date_month = '10' 167 | elif birth_date_month[:3].upper() in ['NOV']: 168 | birth_date_month = '11' 169 | elif birth_date_month[:3].upper() in ['DEZ', 'DEC', 'DIC']: 170 | birth_date_month = '12' 171 | else: 172 | birth_date_month = birth_date_month[:3].upper() 173 | 174 | verify_request_message = { 175 | 'MsgType': 'B8', 176 | 'VerifyCustomerReqID':req_id, 177 | 'ClientID': user_id, 178 | 'BrokerID': broker_id, 179 | 'VerificationData': { 180 | 'formID': formID, 181 | 'submissionID': submissionID, 182 | 'created_at': createdAt, 183 | 'name': { 184 | 'first': first_name, 185 | 'middle': middle_name, 186 | 'last': last_name, 187 | }, 188 | 'address': { 189 | 'street1': address_addr_line1, 190 | 'street2': address_addr_line2, 191 | 'city': address_city, 192 | 'state': address_state, 193 | 'postal_code': address_postal, 194 | 'country': address_country, 195 | 'country_code': address_country_code, 196 | }, 197 | 'phone_number': phone_number_country + phone_number_area + phone_number_phone, 198 | 'date_of_birth': birth_date_year + '-' + birth_date_month + '-' + birth_date_day, 199 | 'uploaded_files': uploaded_files 200 | }, 201 | 'Verify': 1 202 | } 203 | 204 | if finger_print: 205 | verify_request_message['VerificationData']['browser_finger_print'] = finger_print 206 | 207 | try: 208 | if stunt_ip: 209 | verify_request_message['VerificationData']['stunt_ip'] = json.loads(stunt_ip) 210 | except: 211 | pass 212 | 213 | for field in id_fields: 214 | for key, value in raw_request.iteritems(): 215 | if field in key: 216 | field_name = camel_to_underscore(field) 217 | if 'identification' not in verify_request_message['VerificationData']: 218 | verify_request_message['VerificationData']['identification'] = {} 219 | verify_request_message['VerificationData']['identification'][ field_name ] = value 220 | 221 | verify_request_message['VerificationData'] = json.dumps(verify_request_message['VerificationData']) 222 | 223 | self.application.application_trade_client.sendJSON(verify_request_message) 224 | self.write('*ok*') 225 | --------------------------------------------------------------------------------