├── README.md ├── config.py ├── observers ├── logger.py ├── observer.py ├── traderbot.py └── traderbotsim.py ├── private_markets ├── huobi_main.py ├── huobi_new.py └── huobi_pro.py ├── public_markets ├── huobicny.py └── market.py ├── run.py ├── single.py ├── triangular.py └── utils.py /README.md: -------------------------------------------------------------------------------- 1 | # python写的虚拟币三角套利程序 2 | 3 | 原理: 4 | 三角套利是利用多种汇价在不同市场间的差价进行套利动作。例如在A银行美元以荷兰币表示的汇价为1.9025fl/$,B银行美元以加拿大币表示的价格为1.2646c$/$,C银行加拿大币以荷兰币基尔德表示的价格为1.5241fl/c$。依据A与B银行的报价,可依据计算交叉汇率的方式,得到加拿大币以基尔德表示的价格为:﹝1.9025fl/$﹞/﹝1.2646c$/$﹞=1.5044fl/c$ 5 | 所得之交叉汇率水准C银行之加拿大币的汇价有所差异,这意味着套利空间的存在。因为C银行加拿大币的汇价高于经交叉汇率算出的结果,这显示一单位加拿大币在C银行价值比较高,因此若经A、B银行间的交易以1.5044基尔德兑换一单位加拿大币,在将此一加拿大币与C银行换回1.5214基尔德,这一来一往间便获取0.017基尔德的三角套利。 6 | 三角套利过程:透过交叉汇率计算出的汇价,与市场上实际汇价有所差异,造成市场存在套利空间。市场人士可同时在不同银行间买卖外币,进行所谓的三角套利。 7 | 8 | 参考老外用php实现的btc-e平台三套套利交易程序 9 | https://bitcointalk.org/index.php?topic=236321.0 10 | 11 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | market = 'HuobiCNY' 2 | market_expiration_time = 10 3 | #按照['btc_cny', 'eth_btc', 'eth_cny']这个顺序写 4 | currency_pairs = { 5 | 'bcc':['btc_cny', 'bcc_btc', 'bcc_cny'], 6 | } 7 | 8 | min_amount = 0.001 9 | max_amount = 0.001 10 | increment = 0.001 11 | 12 | #loop间隔 s 13 | refresh_rate = 3 14 | #平台所有三角套利的币种 15 | symbols = ['bcc'] 16 | 17 | observers = [ 18 | # 'TraderBotSim', 19 | 'Logger', 20 | # 'TraderBot', 21 | ] 22 | 23 | min_profit = 0.013 24 | fee = 0.002 25 | slippage = 1.002 26 | 27 | #在此输入您的Key 28 | ACCESS_KEY = '' 29 | SECRET_KEY = '' 30 | 31 | 32 | -------------------------------------------------------------------------------- /observers/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .observer import Observer 3 | 4 | class Logger(Observer): 5 | def opportunity(self, item): 6 | logging.info('%s Amount %f Expected Profit %f' % (item['case'], item['amount'], item['profit']) ) -------------------------------------------------------------------------------- /observers/observer.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | class Observer(metaclass=abc.ABCMeta): 4 | def begin_opportunity_finder(self, depths, fee): 5 | pass 6 | 7 | def end_opportunity_finder(self): 8 | pass 9 | 10 | @abc.abstractmethod 11 | def opportunity(self, item): 12 | pass -------------------------------------------------------------------------------- /observers/traderbot.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import config 3 | import time 4 | from .observer import Observer 5 | import sys 6 | from private_markets import huobi_main as main, huobi_new as new, huobi_pro as pro 7 | 8 | # second 9 | ORDER_EXPIRATION_TIME = 20 10 | 11 | class TraderBot(Observer): 12 | 13 | def opportunity(self, item): 14 | logging.info('%s Amount %f Expected Profit %f' % (item[0][1]['case'], item[0][1]['amount'], item[0][1]['profit']) ) 15 | if item[0][1]['best_case'] == 1: 16 | #case 1: cny -> btc -> cc -> cny 17 | logging.info('step 1') 18 | trade = item[0][1]['best_trades'][0] 19 | cny_balance = float(eval(main.getAccountInfo(main.ACCOUNT_INFO) )['available_cny_display']) 20 | if float(cny_balance) < float(trade['rate'] * trade['amount']): 21 | trans = self.verify("new.query_transfer('new','main','cny'," + str(round(trade['amount'] * trade['rate'], 2)) + " )", time.time()) 22 | if trans['status'] == 'error': 23 | logging.warn('step 1: new\'s cny transfer main failed %s' % trans['err-msg']) 24 | return 25 | if not self.recur(self.perform_trade(trade,False), time.time() ): 26 | return 27 | 28 | logging.info('step 2') 29 | trade = item[0][1]['best_trades'][1] 30 | trans = self.verify("pro.query_transfer('main', 'pro', 'btc'," + str(trade['transfer']) + " )", time.time()) 31 | if trans['status'] == 'error': 32 | logging.warn('step 2: main\'s btc transfer pro failed %s' % trans['err-msg']) 33 | return 34 | if not self.recur(self.perform_trade(trade,False), time.time() ): 35 | return 36 | 37 | logging.info('step 3') 38 | trade = item[0][1]['best_trades'][2] 39 | if trade['pair'] in ['eth_cny','bcc_cny','etc_cny']: 40 | trans = self.verify("pro.query_transfer('pro', 'new', " + "'"+trade['pair'][:3]+"'" + ", " +str(trade['transfer']) +")", time.time()) 41 | else: 42 | trans = self.verify("pro.query_transfer('pro','main'," + "'"+trade['pair'][:3]+"'" + "," +str(trade['transfer']) + ")", time.time()) 43 | if trans['status'] == 'error': 44 | logging.warn('step 3: transfer failed %s' % trans['err-msg']) 45 | return 46 | if not self.recur(self.perform_trade(trade,False), time.time() ): 47 | return 48 | elif item[0][1]['best_case'] == 2: 49 | #case 2: cny -> cc -> btc -> cny 50 | logging.info('step 1') 51 | trade = item[0][1]['best_trades'][0] 52 | if trade['pair'] in ['eth_cny','bcc_cny','etc_cny']: 53 | for x in new.get_account_info()['data']['list']: 54 | if x['currency'] == 'cny' and x['type'] == 'trade': 55 | cny_balance = x['balance'] 56 | if float(cny_balance) < float(trade['rate'] * trade['amount']): 57 | trans = self.verify("new.query_transfer('main', 'new', 'cny', " + str(round(trade['amount'] * trade['rate'], 2)) + " )", time.time()) 58 | if trans['status'] == 'error': 59 | logging.warn('step 1: main\'s cny transfer new failed %s' % trans['err-msg']) 60 | return 61 | if not self.recur(self.perform_trade(trade,False), time.time() ): 62 | return 63 | else: 64 | cny_balance = float(eval(main.getAccountInfo(main.ACCOUNT_INFO) )['available_cny_display']) 65 | if float(cny_balance) < float(trade['rate'] * trade['amount']): 66 | trans = self.verify("new.query_transfer('new','main','cny'," + str(round(trade['amount'] * trade['rate'], 2)) + ")", time.time()) 67 | if trans['status'] == 'error': 68 | logging.warn('step 1: new\'s cny transfer main failed %s' % trans['err-msg']) 69 | return 70 | if not self.recur(self.perform_trade(trade,False), time.time() ): 71 | return 72 | 73 | logging.info('step 2') 74 | trade = item[0][1]['best_trades'][1] 75 | f = 'main' if trade['pair'] == 'ltc_btc' else 'new' 76 | trans = self.verify("pro.query_transfer(f, 'pro', " + "'"+trade['pair'][:3]+"'" + "," + str(trade['amount']) + ")", time.time()) 77 | if trans['status'] == 'error': 78 | logging.warn('step 2: main\'s btc transfer pro failed %s' % trans['err-msg']) 79 | return 80 | if not self.recur(self.perform_trade(trade,False), time.time() ): 81 | return 82 | 83 | logging.info('step 3') 84 | trade = item[0][1]['best_trades'][2] 85 | if trade['pair'] in ['eth_cny','bcc_cny','etc_cny']: 86 | trans = self.verify("pro.query_transfer('pro', 'new', 'btc', " + str(trade['amount']) + ")", time.time()) 87 | else: 88 | trans = self.verify("pro.query_transfer('pro', 'main', 'btc', " + str(trade['amount']) + ")", time.time()) 89 | if trans['status'] == 'error': 90 | logging.warn('step 3: transfer failed %s' % trans['err-msg']) 91 | return 92 | if not self.recur(self.perform_trade(trade,False), time.time() ): 93 | return 94 | 95 | def recur(self, query_order, time_now): 96 | if 'status' in query_order.keys(): 97 | if query_order['status'] == 'ok': 98 | return True 99 | else: 100 | logging.warn('trade failed %s' % query_order['err-msg']) 101 | return False 102 | elif 'result' in query_order.keys(): 103 | if query_order['result'] == 'success': 104 | return True 105 | else: 106 | logging.warn('trade failed %s' % query_order['message']) 107 | return False 108 | 109 | # def recur(self, query_order, time_now): 110 | # if 'status' in query_order.keys() and query_order['status'] == 'ok': 111 | # return True 112 | # else: 113 | # if time.time() - time_now < ORDER_EXPIRATION_TIME: 114 | # self.recur(query_order, time_now) 115 | # if 'code' in query_order.keys(): 116 | # logging.warn('trade failed %s' % query_order['msg']) 117 | # else: 118 | # logging.warn('trade failed, unknown error') 119 | # return False 120 | 121 | def verify(self, trans_str, time_now): 122 | while time.time() - time_now < ORDER_EXPIRATION_TIME: 123 | trans = eval(trans_str) 124 | if trans['status'] == 'ok': 125 | break 126 | return trans 127 | 128 | def perform_trade(self, trade, show_balance): 129 | symbols = { 130 | 'btc_cny':1, 131 | 'eth_btc':'ethbtc', 132 | 'eth_cny':'ethcny', 133 | 'ltc_btc':'ltcbtc', 134 | 'ltc_cny':2, 135 | 'bcc_btc':'bccbtc', 136 | 'bcc_cny':'bcccny', 137 | 'etc_btc':'etcbtc', 138 | 'etc_cny':'etccny', 139 | } 140 | 141 | if trade['pair'] in ['btc_cny', 'ltc_cny']: 142 | query_order = eval(main.query_order(symbols[trade['pair']],trade['rate'],trade['amount'],None,None,trade['type']) ) 143 | 144 | if trade['pair'] in ['eth_btc','ltc_btc','bcc_btc','etc_btc']: 145 | t = 'buy-limit' if trade['type'] == 'buy' else 'sell-limit' 146 | query_order = pro.query_order(trade['amount'], trade['rate'], symbols[trade['pair']], t) 147 | 148 | if trade['pair'] in ['eth_cny','bcc_cny','etc_cny']: 149 | t = 'buy-limit' if trade['type'] == 'buy' else 'sell-limit' 150 | query_order = new.query_order(trade['amount'], trade['rate'], symbols[trade['pair']], t) 151 | 152 | return query_order 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /observers/traderbotsim.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import config 3 | import time 4 | from .observer import Observer 5 | import sys 6 | from private_markets import huobi_main as main, huobi_new as new, huobi_pro as pro 7 | 8 | # second 9 | ORDER_EXPIRATION_TIME = 20 10 | 11 | class TraderBotSim(Observer): 12 | 13 | def opportunity(self, item): 14 | logging.info('%s Amount %f Expected Profit %f' % (item['case'], item['amount'], item['profit']) ) 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /private_markets/huobi_main.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import time 3 | import urllib 4 | import urllib.parse 5 | import urllib.request 6 | from config import ACCESS_KEY,SECRET_KEY 7 | 8 | 9 | HUOBI_SERVICE_API="https://api.huobi.com/apiv3" 10 | ACCOUNT_INFO = "get_account_info" 11 | GET_ORDERS = "get_orders" 12 | ORDER_INFO = "order_info" 13 | BUY = "buy" 14 | BUY_MARKET = "buy_market" 15 | CANCEL_ORDER = "cancel_order" 16 | NEW_DEAL_ORDERS = "get_new_deal_orders" 17 | ORDER_ID_BY_TRADE_ID = "get_order_id_by_trade_id" 18 | SELL = "sell" 19 | SELL_MARKET = "sell_market" 20 | 21 | ''' 22 | 发送信息到api 23 | ''' 24 | def send2api(pParams, extra): 25 | pParams['access_key'] = ACCESS_KEY 26 | pParams['created'] = int(time.time()) 27 | pParams['sign'] = createSign(pParams) 28 | if(extra) : 29 | for k in extra: 30 | v = extra.get(k) 31 | if(v != None): 32 | pParams[k] = v 33 | #pParams.update(extra) 34 | tResult = httpRequest(HUOBI_SERVICE_API, pParams) 35 | return tResult 36 | 37 | ''' 38 | 生成签名 39 | ''' 40 | def createSign(params): 41 | params['secret_key'] = SECRET_KEY; 42 | params = sorted(params.items(), key=lambda d:d[0], reverse=False) 43 | message = urllib.parse.urlencode(params) 44 | message=message.encode(encoding='UTF8') 45 | m = hashlib.md5() 46 | m.update(message) 47 | m.digest() 48 | sig=m.hexdigest() 49 | return sig 50 | 51 | ''' 52 | request 53 | ''' 54 | def httpRequest(url, params): 55 | postdata = urllib.parse.urlencode(params) 56 | postdata = postdata.encode('utf-8') 57 | 58 | fp = urllib.request.urlopen(url, postdata) 59 | if fp.status != 200 : 60 | return None 61 | else: 62 | mybytes = fp.read() 63 | mystr = mybytes.decode("utf8") 64 | fp.close() 65 | return mystr 66 | 67 | ''' 68 | 获取账号详情 69 | ''' 70 | def getAccountInfo(method): 71 | params = {"method":method} 72 | extra = {} 73 | res = send2api(params, extra) 74 | return res 75 | 76 | ''' 77 | 限价 78 | @param coinType 79 | @param price 80 | @param amount 81 | @param tradePassword 82 | @param tradeid 83 | @param method 84 | ''' 85 | def query_order(coinType,price,amount,tradePassword,tradeid,method): 86 | params = {"method":method} 87 | params['coin_type'] = coinType 88 | params['price'] = price 89 | params['amount'] = amount 90 | extra = {} 91 | extra['trade_password'] = tradePassword 92 | extra['trade_id'] = tradeid 93 | res = send2api(params, extra) 94 | return res -------------------------------------------------------------------------------- /private_markets/huobi_new.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hmac 3 | import hashlib 4 | import urllib 5 | import json 6 | import time 7 | from urllib import parse 8 | from urllib import request 9 | from datetime import datetime 10 | from config import ACCESS_KEY,SECRET_KEY 11 | 12 | # timeout in 5 seconds: 13 | TIMEOUT = 5 14 | 15 | ACCOUNT_ID = 16 | 17 | API_HOST = 'be.huobi.com' 18 | 19 | SCHEME = 'https' 20 | 21 | # language setting: 'zh-CN', 'en': 22 | LANG = 'zh-CN' 23 | 24 | DEFAULT_GET_HEADERS = { 25 | 'Accept': 'application/json', 26 | 'Accept-Language': LANG, 27 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36' 28 | } 29 | 30 | DEFAULT_POST_HEADERS = { 31 | 'Content-Type': 'application/json', 32 | 'Accept': 'application/json', 33 | 'Accept-Language': LANG, 34 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36' 35 | } 36 | 37 | class ApiClient(object): 38 | 39 | def __init__(self, appKey, appSecret, assetPassword=None, host=API_HOST): 40 | ''' 41 | Init api client object, by passing appKey and appSecret. 42 | ''' 43 | self._accessKeyId = appKey 44 | self._accessKeySecret = appSecret.encode('utf-8') # change to bytes 45 | self._assetPassword = assetPassword 46 | self._host = host 47 | 48 | def get(self, path, **params): 49 | qs = self._sign('GET', path, self._utc(), params) 50 | return self._call('GET', '%s?%s' % (path, qs)) 51 | 52 | def post(self, path, obj=None): 53 | qs = self._sign('POST', path, self._utc()) 54 | data = None 55 | if obj is not None: 56 | data = json.dumps(obj).encode('utf-8') 57 | return self._call('POST', '%s?%s' % (path, qs), data) 58 | 59 | def _call(self, method, uri, data=None): 60 | url = '%s://%s%s' % (SCHEME, self._host, uri) 61 | # print(method + ' ' + url) 62 | headers = DEFAULT_GET_HEADERS if method=='GET' else DEFAULT_POST_HEADERS 63 | req = request.Request(url, data=data, headers=headers, method=method) 64 | with request.urlopen(req, timeout=TIMEOUT) as resp: 65 | if resp.getcode()!=200: 66 | raise ApiNetworkError('Bad response code: %s %s' % (resp.getcode(), resp.reason)) 67 | # type dict 68 | return json.loads(resp.read().decode('utf-8')) 69 | 70 | 71 | def _sign(self, method, path, ts, params=None): 72 | self._method = method 73 | # create signature: 74 | if params is None: 75 | params = {} 76 | params['SignatureMethod'] = 'HmacSHA256' 77 | params['SignatureVersion'] = '2' 78 | params['AccessKeyId'] = self._accessKeyId 79 | params['Timestamp'] = ts 80 | # sort by key: 81 | keys = sorted(params.keys()) 82 | # build query string like: a=1&b=%20&c=: 83 | qs = '&'.join(['%s=%s' % (key, self._encode(params[key])) for key in keys]) 84 | # build payload: 85 | payload = '%s\n%s\n%s\n%s' % (method, self._host, path, qs) 86 | # print('payload:\n%s' % payload) 87 | dig = hmac.new(self._accessKeySecret, msg=payload.encode('utf-8'), digestmod=hashlib.sha256).digest() 88 | sig = self._encode(base64.b64encode(dig).decode()) 89 | # print('sign: ' + sig) 90 | qs = qs + '&Signature=' + sig 91 | return qs 92 | 93 | def _utc(self): 94 | return datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S') 95 | 96 | def _encode(self, s): 97 | return parse.quote(s, safe='') 98 | 99 | 100 | def query_order(amount, price, symbol, _type): 101 | client = ApiClient(ACCESS_KEY, SECRET_KEY) 102 | return client.post('/v1/order/orders/place', { 103 | 'account-id': ACCOUNT_ID, 104 | 'amount': amount, 105 | 'price': price, 106 | 'symbol': symbol, 107 | 'type': _type, 108 | 'source': 'api', 109 | }) 110 | 111 | def query_transfer(_from, to, currency, amount): 112 | client = ApiClient(ACCESS_KEY, SECRET_KEY) 113 | return client.post('/v1/dw/balance/transfer', { 114 | 'from' : _from, 115 | 'to' : to, 116 | 'currency' : currency, 117 | 'amount' : amount, 118 | }) 119 | 120 | def get_account(): 121 | client = ApiClient(ACCESS_KEY, SECRET_KEY) 122 | return client.get('/v1/account/accounts') 123 | 124 | def get_account_info(): 125 | client = ApiClient(ACCESS_KEY, SECRET_KEY) 126 | return client.get('/v1/account/accounts/' + str(ACCOUNT_ID) + '/balance') 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /private_markets/huobi_pro.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hmac 3 | import hashlib 4 | import urllib 5 | import json 6 | import time 7 | from urllib import parse 8 | from urllib import request 9 | from datetime import datetime 10 | from config import ACCESS_KEY,SECRET_KEY 11 | 12 | # timeout in 5 seconds: 13 | TIMEOUT = 5 14 | 15 | ACCOUNT_ID = 16 | 17 | API_HOST = 'api.huobi.pro' 18 | 19 | SCHEME = 'https' 20 | 21 | # language setting: 'zh-CN', 'en': 22 | LANG = 'zh-CN' 23 | 24 | DEFAULT_GET_HEADERS = { 25 | 'Accept': 'application/json', 26 | 'Accept-Language': LANG, 27 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36' 28 | } 29 | 30 | DEFAULT_POST_HEADERS = { 31 | 'Content-Type': 'application/json', 32 | 'Accept': 'application/json', 33 | 'Accept-Language': LANG, 34 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36' 35 | } 36 | 37 | class ApiClient(object): 38 | 39 | def __init__(self, appKey, appSecret, assetPassword=None, host=API_HOST): 40 | ''' 41 | Init api client object, by passing appKey and appSecret. 42 | ''' 43 | self._accessKeyId = appKey 44 | self._accessKeySecret = appSecret.encode('utf-8') # change to bytes 45 | self._assetPassword = assetPassword 46 | self._host = host 47 | 48 | def get(self, path, **params): 49 | ''' 50 | Send a http get request and return json object. 51 | ''' 52 | qs = self._sign('GET', path, self._utc(), params) 53 | return self._call('GET', '%s?%s' % (path, qs)) 54 | 55 | def post(self, path, obj=None): 56 | qs = self._sign('POST', path, self._utc()) 57 | data = None 58 | if obj is not None: 59 | data = json.dumps(obj).encode('utf-8') 60 | return self._call('POST', '%s?%s' % (path, qs), data) 61 | 62 | def _call(self, method, uri, data=None): 63 | url = '%s://%s%s' % (SCHEME, self._host, uri) 64 | # print(method + ' ' + url) 65 | headers = DEFAULT_GET_HEADERS if method=='GET' else DEFAULT_POST_HEADERS 66 | req = request.Request(url, data=data, headers=headers, method=method) 67 | with request.urlopen(req, timeout=TIMEOUT) as resp: 68 | if resp.getcode()!=200: 69 | raise ApiNetworkError('Bad response code: %s %s' % (resp.getcode(), resp.reason)) 70 | # type dict 71 | return json.loads(resp.read().decode('utf-8')) 72 | 73 | 74 | def _sign(self, method, path, ts, params=None): 75 | self._method = method 76 | # create signature: 77 | if params is None: 78 | params = {} 79 | params['SignatureMethod'] = 'HmacSHA256' 80 | params['SignatureVersion'] = '2' 81 | params['AccessKeyId'] = self._accessKeyId 82 | params['Timestamp'] = ts 83 | # sort by key: 84 | keys = sorted(params.keys()) 85 | # build query string like: a=1&b=%20&c=: 86 | qs = '&'.join(['%s=%s' % (key, self._encode(params[key])) for key in keys]) 87 | # build payload: 88 | payload = '%s\n%s\n%s\n%s' % (method, self._host, path, qs) 89 | # print('payload:\n%s' % payload) 90 | dig = hmac.new(self._accessKeySecret, msg=payload.encode('utf-8'), digestmod=hashlib.sha256).digest() 91 | sig = self._encode(base64.b64encode(dig).decode()) 92 | # print('sign: ' + sig) 93 | qs = qs + '&Signature=' + sig 94 | return qs 95 | 96 | def _utc(self): 97 | return datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S') 98 | 99 | def _encode(self, s): 100 | return parse.quote(s, safe='') 101 | 102 | 103 | def query_order(amount, price, symbol, _type): 104 | client = ApiClient(ACCESS_KEY, SECRET_KEY) 105 | return client.post('/v1/order/orders/place', { 106 | 'account-id': ACCOUNT_ID, 107 | 'amount': amount, 108 | 'price': price, 109 | 'symbol': symbol, 110 | 'type': _type, 111 | 'source': 'api', 112 | }) 113 | 114 | def query_transfer(_from, to, currency, amount): 115 | client = ApiClient(ACCESS_KEY, SECRET_KEY) 116 | return client.post('/v1/dw/balance/transfer', { 117 | 'from' : _from, 118 | 'to' : to, 119 | 'currency' : currency, 120 | 'amount' : amount, 121 | }) 122 | 123 | def get_account(): 124 | client = ApiClient(ACCESS_KEY, SECRET_KEY) 125 | return client.get('/v1/account/accounts') 126 | 127 | 128 | -------------------------------------------------------------------------------- /public_markets/huobicny.py: -------------------------------------------------------------------------------- 1 | import urllib.request 2 | import urllib.error 3 | import urllib.parse 4 | import json 5 | from .market import Market 6 | 7 | class HuobiCNY(Market): 8 | def __init__(self): 9 | super().__init__() 10 | self.depths = { 11 | 'btc_cny' : 'http://api.huobi.com/staticmarket/depth_btc_10.js', 12 | 'ltc_cny' : 'http://api.huobi.com/staticmarket/depth_ltc_10.js', 13 | 'eth_cny' : 'https://be.huobi.com/market/depth?symbol=ethcny&type=step0', 14 | 'etc_cny' : 'https://be.huobi.com/market/depth?symbol=etccny&type=step0', 15 | 'bcc_cny' : 'https://be.huobi.com/market/depth?symbol=bcccny&type=step0', 16 | 'eth_btc' : 'https://api.huobi.pro/market/depth?symbol=ethbtc&type=step0', 17 | 'bcc_btc' : 'https://api.huobi.pro/market/depth?symbol=bccbtc&type=step0', 18 | 'etc_btc' : 'https://api.huobi.pro/market/depth?symbol=etcbtc&type=step0', 19 | 'ltc_btc' : 'https://api.huobi.pro/market/depth?symbol=ltcbtc&type=step0', 20 | } 21 | 22 | def update_depth(self, symbol): 23 | url = self.depths[symbol]; 24 | req = urllib.request.Request(url,headers={ 25 | "Content-Type": "application/json", 26 | "Accept": "*/*", 27 | "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36"}) 28 | res = urllib.request.urlopen(req) 29 | depth = json.loads(res.read().decode('utf8')) 30 | self.depth = self.format_depth(depth) 31 | # print(self.depth) 32 | 33 | def sort_and_format(self, l, reverse=False): 34 | l.sort(key=lambda x: float(x[0]), reverse=reverse) 35 | r = [] 36 | for i in l: 37 | r.append({'price': float(i[0]), 'amount': float(i[1])}) 38 | return r 39 | 40 | def format_depth(self, depth): 41 | if 'tick' in depth.keys(): 42 | depth['bids'] = depth['tick']['bids'] 43 | depth['asks'] = depth['tick']['asks'] 44 | bids = self.sort_and_format(depth['bids'], True) 45 | asks = self.sort_and_format(depth['asks'], False) 46 | return {'asks':asks, 'bids':bids} 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /public_markets/market.py: -------------------------------------------------------------------------------- 1 | import time 2 | import urllib.request 3 | import urllib.error 4 | import urllib.parse 5 | import config 6 | import logging 7 | from utils import log_exception 8 | 9 | class Market(): 10 | def __init__(self): 11 | self.name = self.__class__.__name__ 12 | self.depth_updated = 0 13 | self.update_rate = 2 14 | 15 | def get_depth(self, pair): 16 | timediff = time.time() - self.depth_updated 17 | if timediff > self.update_rate: 18 | self.ask_update_depth(pair) 19 | timediff = time.time() - self.depth_updated 20 | 21 | if timediff > config.market_expiration_time: 22 | logging.warn('Market: %s order book is expired' % self.name) 23 | self.depth = {'asks': [{'price': 0, 'amount': 0}], 'bids': [ 24 | {'price': 0, 'amount': 0}]} 25 | return self.depth 26 | 27 | def ask_update_depth(self, pair): 28 | try: 29 | self.update_depth(pair) 30 | self.depth_updated = time.time() 31 | except (urllib.error.HTTPError, urllib.error.URLError) as e: 32 | logging.error("HTTPError, can't update market: %s" % self.name) 33 | log_exception(logging.DEBUG) 34 | except Exception as e: 35 | logging.error("Can't update market: %s - %s" % (self.name, str(e))) 36 | log_exception(logging.DEBUG) 37 | 38 | def update_depth(self): 39 | pass 40 | 41 | def buy(self, price, amount): 42 | pass 43 | 44 | def sell(self, price, amount): 45 | pass 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import logging 2 | # import argparse 3 | import sys 4 | # from triangular import Triangular 5 | from single import Single 6 | 7 | class Run(): 8 | def __init__(self): 9 | pass 10 | 11 | def create_triangular(self): 12 | self.triangular = Triangular() 13 | 14 | def exec_command(self): 15 | # self.create_triangular() 16 | # self.triangular.loop() 17 | self.single = Single() 18 | self.single.loop() 19 | 20 | def init_logger(self): 21 | level = logging.DEBUG 22 | logging.basicConfig(format='%(asctime)s [%(levelname)s] %(message)s', level=level) 23 | 24 | def main(self): 25 | self.init_logger() 26 | self.exec_command() 27 | 28 | 29 | def main(): 30 | cli = Run() 31 | cli.main() 32 | 33 | if __name__ == '__main__': 34 | main() 35 | 36 | -------------------------------------------------------------------------------- /single.py: -------------------------------------------------------------------------------- 1 | import config 2 | import sys 3 | import logging 4 | import time 5 | from concurrent.futures import ThreadPoolExecutor, wait 6 | 7 | 8 | class Single(): 9 | def __init__(self): 10 | self.observers = [] 11 | self.init_market(config.market) 12 | self.init_observers(config.observers) 13 | self.symbol = 'bcc' 14 | self.currency_pairs = config.currency_pairs[self.symbol] 15 | self.depths = {} 16 | self.depths2 = {} 17 | self.threadpool = ThreadPoolExecutor(max_workers=3) 18 | 19 | def init_market(self, market): 20 | try: 21 | exec('import public_markets.' + market.lower()) 22 | m = eval('public_markets.' + market.lower() + '.' + market + '()' ) 23 | self.market = m 24 | except(ImportError, AttributeError) as e: 25 | print('%s market name is invalid' % m) 26 | 27 | def init_observers(self, _observers): 28 | for observer_name in _observers: 29 | try: 30 | exec('import observers.' + observer_name.lower()) 31 | observer = eval('observers.' + observer_name.lower() + '.' + observer_name + '()') 32 | self.observers.append(observer) 33 | except(ImportError, AttributeError) as e: 34 | print('%s observer name is invalid' % observer_name) 35 | 36 | def __get_depth(self, pair, depths): 37 | depths[pair] = self.market.get_depth(pair) 38 | 39 | def update_depths(self): 40 | depths = {} 41 | futures = [] 42 | for pair in self.currency_pairs: 43 | futures.append(self.threadpool.submit(self.__get_depth, pair, depths)) 44 | wait(futures, timeout=5) 45 | return depths 46 | 47 | def ask_volume(self, orders, amount): 48 | vol = 0 49 | value = 0 50 | 51 | i = 0 52 | while i < len(orders['asks']) and value < amount: 53 | this_value = min(orders['asks'][i]['price'] * orders['asks'][i]['amount'], amount - value) 54 | this_vol = this_value / orders['asks'][i]['price'] 55 | value += this_value 56 | vol += this_vol 57 | 58 | i += 1 59 | 60 | return vol 61 | 62 | def bid_volume(self, orders, amount): 63 | vol = 0 64 | value = 0 65 | 66 | i = 0 67 | while i < len(orders['bids']) and value < amount: 68 | this_value = min(orders['bids'][i]['amount'], amount - value) 69 | this_vol = this_value * orders['bids'][i]['price'] 70 | value += this_value 71 | vol += this_vol 72 | 73 | i += 1 74 | 75 | return vol 76 | 77 | def best_ask(self, orders): 78 | return orders['asks'][0]['price'] 79 | 80 | def best_bid(self, orders): 81 | return orders['bids'][0]['price'] 82 | 83 | def loop(self): 84 | while True: 85 | try: 86 | self.depths = self.update_depths() 87 | # time.sleep(0.1) 88 | # self.depths2 = self.update_depths() 89 | print(self.depths[self.currency_pairs[0]]) 90 | # print(self.depths2[self.currency_pairs[0]]) 91 | # sys.exit(0) 92 | btc_cny_orders = self.depths[self.currency_pairs[0]] 93 | cc_btc_orders = self.depths[self.currency_pairs[1]] 94 | cc_cny_orders = self.depths[self.currency_pairs[2]] 95 | 96 | except Exception as e: 97 | logging.error("Can't update depths: %s" % str(e)) 98 | continue 99 | if btc_cny_orders and cc_btc_orders and cc_cny_orders: 100 | best_case = 0 101 | best_profit = 0 102 | best_amount = 0 103 | best_trades = {} 104 | 105 | amt = config.min_amount 106 | while amt <= config.max_amount: 107 | #Case 1: BTC -> CC -> CNY -> BTC 108 | c1_cc = self.ask_volume(cc_btc_orders, amt) 109 | c1_cny = self.bid_volume(cc_cny_orders, c1_cc) 110 | c1_btc = self.ask_volume(btc_cny_orders, c1_cny) 111 | 112 | c1_profit = c1_btc - amt 113 | c1_profit_percent = (c1_profit * 100) / amt 114 | 115 | if c1_profit > best_profit and c1_profit_percent > config.min_profit: 116 | best_case = 1 117 | best_profit = c1_profit 118 | best_amount = amt 119 | best_trades = [ 120 | { 121 | 'pair':self.currency_pairs[1], 122 | 'type':'buy', 123 | 'amount':round(c1_cc, 4), 124 | 'rate':round(self.best_ask(cc_btc_orders) * config.slippage, 6) 125 | }, 126 | { 127 | 'pair':self.currency_pairs[2], 128 | 'type':'sell', 129 | 'amount':round(c1_cc, 4), 130 | 'rate':round(self.best_bid(cc_cny_orders) / config.slippage, 2) 131 | }, 132 | { 133 | 'pair':self.currency_pairs[0], 134 | 'type':'buy', 135 | 'amount':round(c1_btc, 4), 136 | 'rate':round(self.best_ask(btc_cny_orders) * config.slippage, 2) 137 | }, 138 | ] 139 | 140 | # Case 2: BTC -> CNY -> CC -> BTC 141 | c2_cny = self.bid_volume(btc_cny_orders, amt) 142 | c2_cc = self.ask_volume(cc_cny_orders, c2_cny) 143 | c2_btc = self.bid_volume(cc_btc_orders, c2_cc) 144 | 145 | c2_profit = c2_btc - amt 146 | c2_profit_percent = (c2_profit * 100) / amt 147 | 148 | if c2_profit > best_profit and c2_profit_percent > config.min_profit: 149 | best_case = 2 150 | best_profit = c2_profit 151 | best_amount = amt 152 | best_trades = [ 153 | { 154 | 'pair':self.currency_pairs[0], 155 | 'type':'sell', 156 | 'amount':round(amt, 4), 157 | 'rate':round(self.best_bid(btc_cny_orders) / config.slippage, 2) 158 | }, 159 | { 160 | 'pair':self.currency_pairs[2], 161 | 'type':'buy', 162 | 'amount':round(c2_cc, 4), 163 | 'rate':round(self.best_ask(cc_cny_orders) * config.slippage, 2) 164 | }, 165 | { 166 | 'pair':self.currency_pairs[1], 167 | 'type':'sell', 168 | 'amount':round(c2_cc, 4), 169 | 'rate':round(self.best_bid(cc_btc_orders) * config.slippage, 6) 170 | }, 171 | ] 172 | 173 | amt += config.increment 174 | 175 | if best_case > 0: 176 | case = "btc -> " + self.symbol + " -> cny -> btc" if best_case == 1 else "btc -> cny -> " + self.symbol + " -> btc" 177 | item = { 178 | 'case':case, 179 | 'amount':best_amount, 180 | 'profit':best_profit, 181 | 'best_trades':best_trades, 182 | 'best_case':best_case 183 | } 184 | 185 | for observer in self.observers: 186 | observer.opportunity(item) 187 | time.sleep(config.refresh_rate) -------------------------------------------------------------------------------- /triangular.py: -------------------------------------------------------------------------------- 1 | import config 2 | import sys 3 | import logging 4 | import time 5 | from concurrent.futures import ThreadPoolExecutor, wait 6 | 7 | 8 | class Triangular(): 9 | def __init__(self): 10 | self.observers = [] 11 | self.symbols = config.symbols 12 | self.threadpool = ThreadPoolExecutor(max_workers=4) 13 | self.init_observers(config.observers) 14 | 15 | def init_observers(self, _observers): 16 | for observer_name in _observers: 17 | try: 18 | exec('import observers.' + observer_name.lower()) 19 | observer = eval('observers.' + observer_name.lower() + '.' + observer_name + '()') 20 | self.observers.append(observer) 21 | except(ImportError, AttributeError) as e: 22 | print('%s observer name is invalid' % observer_name) 23 | 24 | def __get_triangle(self, symbol, triangles): 25 | triangles[symbol] = Triangle(symbol).main() 26 | 27 | def update_cases(self): 28 | futures = [] 29 | triangles = {} 30 | for symbol in self.symbols: 31 | futures.append(self.threadpool.submit(self.__get_triangle, symbol, triangles)) 32 | wait(futures, timeout=4) 33 | return triangles 34 | 35 | def loop(self): 36 | while True: 37 | self.triangles = self.update_cases() 38 | # print(self.triangles) 39 | # sys.exit(0) 40 | item = sorted(self.triangles.items(), key=lambda x: x[1]['profit'], reverse=True) 41 | # print(self.triangles) 42 | # sys.exit(0) 43 | # print(item) 44 | if item != [] and item[0][1]['profit'] > 0: 45 | time.sleep(0.1) 46 | item2 = [(item[0][0], Triangle(item[0][0]).main() )] 47 | # print(item) 48 | # print(item2) 49 | # sys.exit(0) 50 | if item[0][1]['profit'] == item2[0][1]['profit']: 51 | for observer in self.observers: 52 | observer.opportunity(item2) 53 | time.sleep(config.refresh_rate) 54 | 55 | 56 | class Triangle(): 57 | def __init__(self, symbol): 58 | self.fee = config.fee 59 | self.slippage = config.slippage 60 | self.symbol = symbol 61 | self.currency_pairs = config.currency_pairs[symbol] 62 | self.depths = {} 63 | self.init_market(config.market) 64 | self.threadpool = ThreadPoolExecutor(max_workers=3) 65 | 66 | def init_market(self, market): 67 | try: 68 | exec('import public_markets.' + market.lower()) 69 | m = eval('public_markets.' + market.lower() + '.' + market + '()' ) 70 | self.market = m 71 | except(ImportError, AttributeError) as e: 72 | print('%s market name is invalid' % m) 73 | 74 | def __get_depth(self, pair, depths): 75 | depths[pair] = self.market.get_depth(pair) 76 | 77 | def update_depths(self): 78 | depths = {} 79 | futures = [] 80 | for pair in self.currency_pairs: 81 | futures.append(self.threadpool.submit(self.__get_depth, pair, depths)) 82 | wait(futures, timeout=0.1) 83 | return depths 84 | 85 | def ask_volume(self, orders, amount): 86 | vol = 0 87 | value = 0 88 | 89 | i = 0 90 | while i < len(orders['asks']) and value < amount: 91 | this_value = min( (orders['asks'][i]['price'] * self.slippage) * orders['asks'][i]['amount'], amount - value) 92 | this_vol = this_value / (orders['asks'][i]['price'] * self.slippage) 93 | value += this_value 94 | vol += this_vol 95 | 96 | i += 1 97 | #(price, amount) 98 | return (value / vol, vol) 99 | 100 | def bid_volume(self, orders, amount): 101 | vol = 0 102 | value = 0 103 | 104 | i = 0 105 | while i < len(orders['bids']) and value < amount: 106 | this_value = min(orders['bids'][i]['amount'], amount - value) 107 | this_vol = this_value * (orders['bids'][i]['price'] * self.slippage) 108 | value += this_value 109 | vol += this_vol 110 | 111 | i += 1 112 | # (卖出总数, 总收入) 113 | return (value, vol) 114 | 115 | def main(self): 116 | self.depths = self.update_depths() 117 | # return self.depths[self.currency_pairs[0]] 118 | # return self.currency_pairs[0] 119 | btc_cny_orders = self.depths[self.currency_pairs[0]] 120 | # return btc_cny_orders['asks'][0][0] 121 | cc_btc_orders = self.depths[self.currency_pairs[1]] 122 | # logging.info('cc_btc_orders %s' % cc_btc_orders) 123 | cc_cny_orders = self.depths[self.currency_pairs[2]] 124 | 125 | if btc_cny_orders and cc_btc_orders and cc_cny_orders: 126 | best_case = 0 127 | best_profit = 0 128 | best_amount = 0 129 | best_trades = {} 130 | 131 | amt = config.min_amount 132 | while amt <= config.max_amount: 133 | #case 1: cny -> btc -> cc -> cny 134 | c1_btc = self.ask_volume(btc_cny_orders, amt) 135 | c1_btc_balance = int( (c1_btc[1] - c1_btc[1] * self.fee) * 10000) / 10000 136 | c1_cc = self.ask_volume(cc_btc_orders, c1_btc_balance) 137 | c1_cc_balance = int( (c1_cc[1] - c1_cc[1] * self.fee) * 10000) / 10000 138 | c1_cny = self.bid_volume(cc_cny_orders, c1_cc_balance) 139 | c1_cny_balance = c1_cny[1] - c1_cny[1] * self.fee 140 | c1_profit = c1_cny_balance - amt 141 | 142 | if c1_profit > best_profit: 143 | best_case = 1 144 | best_profit = c1_profit 145 | best_amount = amt 146 | best_trades = [ 147 | { 148 | 'pair':self.currency_pairs[0], 149 | 'type':'buy', 150 | 'amount':round(c1_btc[1], 4), 151 | 'rate':round(c1_btc[0], 2) 152 | 153 | }, 154 | { 155 | 'pair':self.currency_pairs[1], 156 | 'type':'buy', 157 | 'amount':round(c1_cc[1], 4), 158 | 'rate':round(c1_cc[0], 6), 159 | 'transfer':c1_btc_balance 160 | }, 161 | { 162 | 'pair':self.currency_pairs[2], 163 | 'type':'sell', 164 | 'amount':round(c1_cny[0], 4), 165 | 'rate':round(c1_cny[1] / c1_cny[0], 2), 166 | 'transfer':c1_cc_balance 167 | }, 168 | ] 169 | ''' 170 | #case 2: cny -> cc -> btc -> cny 171 | c2_cc = self.ask_volume(cc_cny_orders, amt) 172 | # c2_cc_balance = int ( (c2_cc[1] - c2_cc[1] * self.fee) * 10000) / 10000 173 | c2_cc_balance = c2_cc[1] - c2_cc[1] * self.fee 174 | c2_btc = self.bid_volume(cc_btc_orders, c2_cc_balance) 175 | c2_btc_balance = c2_btc[1] - c2_btc[1] * self.fee 176 | c2_cny = self.bid_volume(btc_cny_orders, c2_btc_balance) 177 | c2_cny_balance = c2_cny[1] - c2_cny[1] * self.fee 178 | c2_profit = c2_cny_balance - amt 179 | 180 | if c2_profit > best_profit: 181 | best_case = 2 182 | best_profit = c2_profit 183 | best_amount = amt 184 | best_trades = [ 185 | { 186 | 'pair':self.currency_pairs[2], 187 | 'type':'buy', 188 | 'amount':round(c2_cc[1], 4), 189 | 'rate':round(c2_cc[0], 2) 190 | }, 191 | { 192 | 'pair':self.currency_pairs[1], 193 | 'type':'sell', 194 | 'amount':round(c2_cc_balance, 4), 195 | 'rate':round(c2_btc[1] / c2_cc_balance, 6) 196 | }, 197 | { 198 | 'pair':self.currency_pairs[0], 199 | 'type':'sell', 200 | 'amount':round(c2_btc_balance, 4), 201 | 'rate':round(c2_cny[1] / c2_btc_balance, 2) 202 | }, 203 | ] 204 | ''' 205 | 206 | amt += config.increment 207 | 208 | 209 | if best_case > 0: 210 | case = "cny -> btc -> " + self.symbol + " -> cny" if best_case == 1 else "cny -> " + self.symbol + " -> btc -> cny" 211 | return { 212 | 'case':case, 213 | 'amount':best_amount, 214 | 'profit':best_profit, 215 | 'best_trades':best_trades, 216 | 'best_case':best_case 217 | } 218 | 219 | return { 220 | 'case':0, 221 | 'amount':0, 222 | 'profit':0, 223 | 'best_trades':[], 224 | 'best_case':0 225 | } 226 | 227 | 228 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import traceback 4 | import logging 5 | 6 | def log_exception(level): 7 | exc_type, exc_value, exc_traceback = sys.exc_info() 8 | for i in traceback.extract_tb(exc_traceback): 9 | line = (i[0], i[1], i[2]) 10 | logging.log(level, 'File "%s", line %d, in %s' % line) 11 | logging.log(level, '\t%s' % i[3]) 12 | --------------------------------------------------------------------------------