├── .gitignore ├── MANIFEST.in ├── README.md ├── arbitrage ├── __init__.py ├── arbitrage.py ├── arbitrer.py ├── bitstar_test.py ├── config.py-example ├── fiatconverter.py ├── lib │ ├── __init__.py │ ├── bitstar_sdk.py │ ├── broker.thrift │ ├── broker_api.py │ ├── exchange.py │ ├── helpers.py │ ├── push.py │ └── settings.py ├── observers │ ├── __init__.py │ ├── balancedumper.py │ ├── basicbot.py │ ├── bitstar_mm.py │ ├── btccpro_okspot.py │ ├── emailer.py │ ├── hedgerbot.py │ ├── historydumper.py │ ├── logger.py │ ├── marketmaker.py │ ├── observer.py │ ├── specializedtraderbot.py │ ├── traderbot.py │ ├── traderbotsim.py │ └── xmppmessager.py ├── private_markets │ ├── __init__.py │ ├── bitfinex_bch_btc.py │ ├── bitstampusd.py │ ├── bitstarcny.py │ ├── brokercny.py │ ├── btccprocny.py │ ├── haobtccny.py │ ├── huobicny.py │ ├── market.py │ ├── okcoincny.py │ └── paymium.py ├── public_markets │ ├── __init__.py │ ├── _bitfinex.py │ ├── _bitstar.py │ ├── _bittrex.py │ ├── _huobi.py │ ├── _okcoin.py │ ├── bitfinex_bch_btc.py │ ├── bitfinex_btc_usd.py │ ├── bitstampusd.py │ ├── bitstar_standardcny.py │ ├── bitstarcny.py │ ├── bittrex_bch_btc.py │ ├── brokercny.py │ ├── btceusd.py │ ├── haobtccny.py │ ├── huobicny.py │ ├── market.py │ └── okcoincny.py ├── test │ ├── arbitrage_speed_test.py │ └── arbitrage_test.py └── utils.py ├── docs └── add-new-exchange.md ├── requirements.txt ├── setup.py └── tools └── autopep8-project.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib64 19 | 20 | # Installer logs 21 | pip-log.txt 22 | 23 | # Unit test / coverage reports 24 | .coverage 25 | .tox 26 | nosetests.xml 27 | 28 | # Translations 29 | *.mo 30 | 31 | # Mr Developer 32 | .mr.developer.cfg 33 | .project 34 | .pydevproject 35 | 36 | # dumped data 37 | *.json 38 | 39 | # Project 40 | config.py 41 | config_local.py 42 | config_*.py 43 | old/ 44 | tmp/ 45 | local_settings.py 46 | __pycache__ 47 | .DS_Store 48 | arbitragerbot.py 49 | *.csv 50 | *.dta 51 | trade_history 52 | balance_history 53 | download.sh 54 | *.log 55 | *.do 56 | stata/ 57 | # marketmaker.py 58 | *.sublime-* 59 | 60 | 61 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | recursive-include doc *.md 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bitcoin-arbitrage - opportunity detector and automated trading 2 | 3 | It gets order books from supported exchanges and calculate arbitrage 4 | opportunities between each markets. It takes market depth into account. 5 | 6 | Currently supported exchanges to get data: 7 | - Bitstamp (USD) 8 | - BTC-e (USD) 9 | - Bitfinex (USD) 10 | - Kraken (USD, EUR) 11 | - OkCoin (CNY) 12 | - Gemini (USD) 13 | - BTCC (CNY) 14 | - Coinbase Exchange (USD) 15 | - Huobi (CNY) 16 | 17 | Currently supported exchanges to automate trade: 18 | - Bitstamp (USD) 19 | - OkCoin (CNY) 20 | - Huobi (CNY) 21 | - Bitstar (Future) 22 | - Bitfinex (BCH_BTC) 23 | - Bitfinex (BTC_USD) 24 | 25 | 26 | # WARNING 27 | 28 | **Real trading bots are included. Don't put your API keys in config.py 29 | if you don't know what you are doing.** 30 | 31 | # Installation And Configuration 32 | 33 | $ cp arbitrage/config.py-example arbitrage/config.py 34 | 35 | Then edit config.py file to setup your preferences: watched markets 36 | and observers 37 | 38 | You need Python3 to run this program. To install on Debian, Ubuntu, or 39 | variants of them, use: 40 | 41 | $ sudo apt-get install python3 python3-pip python-nose 42 | $ pip3 install requests zmq 43 | 44 | You need market broker service, please read its README to install then run it. 45 | 46 | https://github.com/philsong/bitcoin-broker 47 | 48 | To connect the broker server you will need to install thriftpy: 49 | 50 | $ pip3 install cython thriftpy 51 | 52 | To use the observer XMPPMessager you will need to install sleekxmpp: 53 | 54 | $ pip3 install sleekxmpp 55 | 56 | # Run 57 | 58 | To run the opportunity watcher: 59 | 60 | $ python3 arbitrage/arbitrage.py watch -v 61 | 62 | To check your balance on an exchange (also a good way to check your accounts configuration): 63 | 64 | $ python3 arbitrage/arbitrage.py -m HaobtcCNY get-balance 65 | $ python3 arbitrage/arbitrage.py -m Bitfinex_BCH_BTC get-balance 66 | $ python3 arbitrage/arbitrage.py -m HaobtcCNY,BitstampUSD get-balance 67 | $ python3 arbitrage/arbitrage.py -m HaobtcCNY,OkCoinCNY,HuobiCNY get-balance 68 | 69 | Run tests 70 | 71 | $ nosetests arbitrage/ 72 | 73 | # Alternative usage 74 | 75 | List supported public markets: 76 | 77 | $ python3 arbitrage/arbitrage.py list-public-markets 78 | 79 | Help 80 | 81 | $ python3 arbitrage/arbitrage.py -h 82 | 83 | # Example 84 | 85 | arbitrage in haobtc, huobi or okcoin 86 | 87 | $ python3 arbitrage/arbitrage.py -oTraderBot -mHaobtcCNY,HuobiCNY 88 | $ python3 arbitrage/arbitrage.py -oTraderBot -mHaobtcCNY,OKCoinCNY 89 | 90 | balance statatistic 91 | 92 | $ python3 arbitrage/arbitrage.py -oBalanceDumper -mHaobtcCNY 93 | 94 | bistar test 95 | 96 | $ python3 arbitrage/bitstar_test.py 97 | 98 | 99 | # TODO 100 | 101 | * Tests 102 | * Write documentation 103 | * Add other exchanges: 104 | * okex 105 | * Update order books with a WebSocket client for supported exchanges 106 | * Better history handling for observer "HistoryDumper" (Redis ?) 107 | * Move EUR / USD from a market to an other: 108 | * Coupons 109 | * Negative Operations 110 | * use Ethercoin or other cryptocurrencies for triangular arbitrage 111 | 112 | # LICENSE 113 | 114 | 115 | MIT 116 | 117 | Copyright (c) 2016 Phil Song 118 | 119 | 120 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 121 | 122 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 123 | 124 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 125 | 126 | ## Donate 127 | 128 | If for some reason you feel like donating a few micro btc to future development, those should go here: 129 | 130 | `1NDnnWCUu926z4wxA3sNBGYWNQD3mKyes8` 131 | 132 | ## Credits 133 | 134 | * @[Maxime Biais](https://github.com/maxme) for [the original work on **bitcoin-arbitrage**](https://github.com/maxme/https://github.com/maxme/bitcoin-arbitrage) 135 | -------------------------------------------------------------------------------- /arbitrage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artooze/crypto-arbitrager/49aaac9255953cb6c36ab16b09d024afd0dd0935/arbitrage/__init__.py -------------------------------------------------------------------------------- /arbitrage/arbitrage.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013, Maxime Biais 2 | # Copyright (C) 2016, Phil Song 3 | 4 | import logging 5 | import argparse 6 | import sys 7 | import public_markets 8 | import glob 9 | import os 10 | import inspect 11 | from arbitrer import Arbitrer 12 | from logging.handlers import RotatingFileHandler 13 | import lib.broker_api as exchange_api 14 | from observers.emailer import send_email 15 | import datetime 16 | import time 17 | import config 18 | import traceback 19 | 20 | class ArbitrerCLI: 21 | def __init__(self): 22 | self.inject_verbose_info() 23 | 24 | def inject_verbose_info(self): 25 | logging.VERBOSE = 15 26 | logging.verbose = lambda x: logging.log(logging.VERBOSE, x) 27 | logging.addLevelName(logging.VERBOSE, "VERBOSE") 28 | 29 | def exec_command(self, args): 30 | logging.debug('exec_command:%s' % args) 31 | if "watch" in args.command: 32 | self.create_arbitrer(args) 33 | self.arbitrer.loop() 34 | if "replay-history" in args.command: 35 | self.create_arbitrer(args) 36 | self.arbitrer.replay_history(args.replay_history) 37 | if "get-balance" in args.command: 38 | self.get_balance(args) 39 | if "list-public-markets" in args.command: 40 | self.list_markets() 41 | if "get-broker-balance" in args.command: 42 | self.get_broker_balance(args) 43 | 44 | def list_markets(self): 45 | logging.debug('list_markets') 46 | for filename in glob.glob(os.path.join(public_markets.__path__[0], "*.py")): 47 | module_name = os.path.basename(filename).replace('.py', '') 48 | if not module_name.startswith('_'): 49 | module = __import__("public_markets." + module_name) 50 | test = eval('module.' + module_name) 51 | for name, obj in inspect.getmembers(test): 52 | if inspect.isclass(obj) and 'Market' in (j.__name__ for j in obj.mro()[1:]): 53 | if not obj.__module__.split('.')[-1].startswith('_'): 54 | print(obj.__name__) 55 | sys.exit(0) 56 | 57 | def get_balance(self, args): 58 | if not args.markets: 59 | logging.error("You must use --markets argument to specify markets") 60 | sys.exit(2) 61 | pmarkets = args.markets.split(",") 62 | pmarketsi = [] 63 | for pmarket in pmarkets: 64 | exec('import private_markets.' + pmarket.lower()) 65 | market = eval('private_markets.' + pmarket.lower() 66 | + '.Private' + pmarket + '()') 67 | pmarketsi.append(market) 68 | for market in pmarketsi: 69 | print(market) 70 | 71 | def get_broker_balance(self, args): 72 | last_email_time = 0 73 | cny_init = config.cny_init 74 | btc_init = config.btc_init 75 | price_init = config.price_init 76 | 77 | exchange_api.init_broker() 78 | while True: 79 | try: 80 | accounts = exchange_api.exchange_get_account() 81 | ticker = exchange_api.exchange_get_ticker() 82 | except Exception as e: 83 | traceback.print_exc() 84 | exchange_api.init_broker() 85 | time.sleep(3) 86 | continue 87 | 88 | if accounts: 89 | cny_balance = 0 90 | btc_balance = 0 91 | cny_frozen = 0 92 | btc_frozen = 0 93 | 94 | broker_msg = '\n----------------------------Hedge Fund statistics------------------------------\n' 95 | broker_msg += 'datetime\t %s\n\n' % str(datetime.datetime.now()) 96 | for account in accounts: 97 | cny_balance += account.available_cny 98 | btc_balance += account.available_btc 99 | cny_frozen += account.frozen_cny 100 | btc_frozen += account.frozen_btc 101 | 102 | broker_msg += "%s:\t\t %s\n" % (account.exchange, str({"cny_balance": account.available_cny, 103 | "btc_balance": account.available_btc, 104 | "cny_frozen": account.frozen_cny, 105 | "btc_frozen": account.frozen_btc})) 106 | broker_msg += '------------------------------------------------------------------------------------\n' 107 | broker_msg += "%s:\t\t %s\n" % ('Asset0', str({"cny": '%.2f' %cny_init, 108 | "btc": '%.2f' %btc_init, "price" : '%.2f' %price_init})) 109 | 110 | broker_msg += "%s:\t\t %s\n" % ('AssetN', str({"cny": '%.2f' %(cny_balance + cny_frozen), 111 | "btc": '%.2f' % (btc_balance+ btc_frozen), "price" : '%.2f' % ticker.bid})) 112 | 113 | cny_total=(btc_balance+btc_frozen)*ticker.bid + cny_balance+cny_frozen 114 | btc_total=btc_balance+btc_frozen + (cny_balance+cny_frozen)/ticker.bid 115 | cny_diff = cny_balance+cny_frozen - cny_init 116 | btc_diff = btc_balance+btc_frozen - btc_init 117 | 118 | btc_bonus = 0 119 | btc_bonus = cny_diff/ ticker.bid 120 | cny_bonus = 0 121 | cny_bonus = btc_diff * ticker.bid 122 | 123 | broker_msg += "%s: %s\n" % ('AssetN CNY Base', str({"cny": '%.2f' % cny_init, 124 | "btc": '%.2f' % (btc_balance+btc_frozen+btc_bonus)})) 125 | 126 | 127 | broker_msg += "%s: %s\n" % ('AssetN BTC Base', str({"cny": '%.2f' % (cny_balance+cny_frozen+cny_bonus), 128 | "btc": '%.2f' % btc_init})) 129 | 130 | broker_msg += "%s:\t\t %s\n" % ('Profit', str({"profit": '%.2fCNY / %.2fBTC' % (cny_diff+cny_bonus, btc_bonus+btc_diff)})) 131 | 132 | broker_msg += '------------------------------------------------------------------------------------\n' 133 | 134 | broker_msg += "%s: %s\n" % ('Asset0 CNY Conv', str({"cny": '%.2f' % (cny_init + btc_init *price_init), 135 | "btc": 0})) 136 | broker_msg += "%s: %s\n" % ('Asset0 BTC Conv', str({"cny": 0, 137 | "btc": '%.2f' % (btc_init + cny_init/price_init)})) 138 | 139 | broker_msg += "%s: %s\n" % ('AssetN CNY Conv', str({"cny": '%.2f' % cny_total, 140 | "btc": 0})) 141 | broker_msg += "%s: %s\n" % ('AssetN BTC Conv', str({"cny": 0, 142 | "btc": '%.2f' % btc_total})) 143 | broker_msg += "%s:\t %s\n" % ('Profit Conv', str({"profit-conv": '%.2fCNY / %.2fBTC' % (cny_total-(cny_init + btc_init *price_init), btc_total-(btc_init + cny_init/price_init))})) 144 | 145 | broker_msg += '\n------------------------------------------------------------------------------------\n' 146 | 147 | logging.info(broker_msg) 148 | 149 | if not args.status: 150 | send_email('Hedge Fund Statistics', broker_msg) 151 | break 152 | if time.time() - last_email_time > 60*10: 153 | last_email_time = time.time() 154 | send_email('Hedge Fund Statistics', broker_msg) 155 | time.sleep(20) 156 | 157 | def create_arbitrer(self, args): 158 | self.arbitrer = Arbitrer() 159 | if args.observers: 160 | self.arbitrer.init_observers(args.observers.split(",")) 161 | if args.markets: 162 | self.arbitrer.init_markets(args.markets.split(",")) 163 | 164 | def init_logger(self, args): 165 | level = logging.INFO 166 | if args.verbose: 167 | level = logging.VERBOSE 168 | if args.debug: 169 | level = logging.DEBUG 170 | logging.basicConfig(format='%(asctime)s [%(levelname)s] %(message)s', 171 | level=level) 172 | 173 | Rthandler = RotatingFileHandler('arbitrage.log', maxBytes=100*1024*1024,backupCount=10) 174 | Rthandler.setLevel(level) 175 | formatter = logging.Formatter('%(asctime)-12s [%(levelname)s] %(message)s') 176 | Rthandler.setFormatter(formatter) 177 | logging.getLogger('').addHandler(Rthandler) 178 | 179 | logging.getLogger("requests").setLevel(logging.WARNING) 180 | logging.getLogger("urllib3").setLevel(logging.WARNING) 181 | 182 | def main(self): 183 | parser = argparse.ArgumentParser() 184 | parser.add_argument("-d", "--debug", help="debug verbose mode", 185 | action="store_true") 186 | parser.add_argument("-v", "--verbose", help="info verbose mode", 187 | action="store_true") 188 | parser.add_argument("-o", "--observers", type=str, 189 | help="observers, example: -oLogger,Emailer") 190 | parser.add_argument("-m", "--markets", type=str, 191 | help="markets, example: -mHaobtcCNY,Bitstamp") 192 | parser.add_argument("-s", "--status", help="status", action="store_true") 193 | parser.add_argument("command", nargs='*', default="watch", 194 | help='verb: "watch|replay-history|get-balance|list-public-markets|get-broker-balance"') 195 | args = parser.parse_args() 196 | self.init_logger(args) 197 | self.exec_command(args) 198 | 199 | def main(): 200 | cli = ArbitrerCLI() 201 | cli.main() 202 | 203 | if __name__ == "__main__": 204 | main() 205 | -------------------------------------------------------------------------------- /arbitrage/arbitrer.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2013, Maxime Biais 4 | # Copyright (C) 2016, Phil Song 5 | 6 | import public_markets 7 | import observers 8 | import config 9 | import time 10 | import logging 11 | import json 12 | from concurrent.futures import ThreadPoolExecutor, wait 13 | import traceback 14 | 15 | import re,sys,re 16 | import string 17 | import signal 18 | 19 | def sigint_handler(signum, frame): 20 | global is_sigint_up 21 | is_sigint_up = True 22 | print ('catched interrupt signal!') 23 | 24 | is_sigint_up = False 25 | 26 | class Arbitrer(object): 27 | def __init__(self): 28 | self.markets = [] 29 | self.observers = [] 30 | self.depths = {} 31 | self.init_markets(config.markets) 32 | self.init_observers(config.observers) 33 | self.threadpool = ThreadPoolExecutor(max_workers=10) 34 | 35 | 36 | def init_markets(self, _markets): 37 | logging.debug("_markets:%s" % _markets) 38 | self.market_names = _markets 39 | for market_name in _markets: 40 | try: 41 | exec('import public_markets.' + market_name.lower()) 42 | market = eval('public_markets.' + market_name.lower() + '.' + 43 | market_name + '()') 44 | self.markets.append(market) 45 | except (ImportError, AttributeError) as e: 46 | print("%s market name is invalid: Ignored (you should check your config file)" % (market_name)) 47 | logging.warn("exception import:%s" % e) 48 | # traceback.print_exc() 49 | 50 | def init_observers(self, _observers): 51 | logging.debug("_observers:%s" % _observers) 52 | 53 | self.observer_names = _observers 54 | for observer_name in _observers: 55 | try: 56 | exec('import observers.' + observer_name.lower()) 57 | observer = eval('observers.' + observer_name.lower() + '.' + 58 | observer_name + '()') 59 | self.observers.append(observer) 60 | except (ImportError, AttributeError) as e: 61 | print("%s observer name is invalid: Ignored (you should check your config file)" % (observer_name)) 62 | # print(e) 63 | 64 | def get_profit_for(self, mi, mj, kask, kbid): 65 | if self.depths[kask]["asks"][mi]["price"] >= self.depths[kbid]["bids"][mj]["price"]: 66 | return 0, 0, 0, 0 67 | 68 | max_amount_buy = 0 69 | for i in range(mi + 1): 70 | max_amount_buy += self.depths[kask]["asks"][i]["amount"] 71 | 72 | max_amount_sell = 0 73 | for j in range(mj + 1): 74 | max_amount_sell += self.depths[kbid]["bids"][j]["amount"] 75 | 76 | max_amount = min(max_amount_buy, max_amount_sell, config.max_tx_volume) 77 | 78 | buy_total = 0 79 | w_buyprice = 0 80 | for i in range(mi + 1): 81 | price = self.depths[kask]["asks"][i]["price"] 82 | amount = min(max_amount, buy_total + self.depths[ 83 | kask]["asks"][i]["amount"]) - buy_total 84 | if amount <= 0: 85 | break 86 | buy_total += amount 87 | if w_buyprice == 0: 88 | w_buyprice = price 89 | else: 90 | w_buyprice = (w_buyprice * (buy_total - amount) + price * amount) / buy_total 91 | 92 | sell_total = 0 93 | w_sellprice = 0 94 | for j in range(mj + 1): 95 | price = self.depths[kbid]["bids"][j]["price"] 96 | amount = min(max_amount, sell_total + self.depths[ 97 | kbid]["bids"][j]["amount"]) - sell_total 98 | if amount < 0: 99 | break 100 | sell_total += amount 101 | if w_sellprice == 0 or sell_total == 0: 102 | w_sellprice = price 103 | else: 104 | w_sellprice = (w_sellprice * ( 105 | sell_total - amount) + price * amount) / sell_total 106 | if abs(sell_total-buy_total) > 0.00001: 107 | logging.warn("sell_total=%s,buy_total=%s", sell_total, buy_total) 108 | 109 | profit = sell_total * w_sellprice - buy_total * w_buyprice 110 | return profit, sell_total, w_buyprice, w_sellprice 111 | 112 | def get_max_depth(self, kask, kbid): 113 | i = 0 114 | if len(self.depths[kbid]["bids"]) != 0 and \ 115 | len(self.depths[kask]["asks"]) != 0: 116 | while self.depths[kask]["asks"][i]["price"] \ 117 | < self.depths[kbid]["bids"][0]["price"]: 118 | if i >= len(self.depths[kask]["asks"]) - 1: 119 | break 120 | # logging.debug("i:%s,%s/%s,%s/%s", i, kask, self.depths[kask]["asks"][i]["price"], 121 | # kbid, self.depths[kbid]["bids"][0]["price"]) 122 | 123 | i += 1 124 | 125 | j = 0 126 | if len(self.depths[kask]["asks"]) != 0 and \ 127 | len(self.depths[kbid]["bids"]) != 0: 128 | while self.depths[kask]["asks"][0]["price"] \ 129 | < self.depths[kbid]["bids"][j]["price"]: 130 | if j >= len(self.depths[kbid]["bids"]) - 1: 131 | break 132 | # logging.debug("j:%s,%s/%s,%s/%s", j, kask, self.depths[kask]["asks"][0]["price"], 133 | # kbid, self.depths[kbid]["bids"][j]["price"]) 134 | 135 | j += 1 136 | 137 | return i, j 138 | 139 | def arbitrage_depth_opportunity(self, kask, kbid): 140 | maxi, maxj = self.get_max_depth(kask, kbid) 141 | best_profit = 0 142 | best_i, best_j = (0, 0) 143 | best_w_buyprice, best_w_sellprice = (0, 0) 144 | best_volume = 0 145 | for i in range(maxi + 1): 146 | for j in range(maxj + 1): 147 | profit, volume, w_buyprice, w_sellprice = self.get_profit_for( 148 | i, j, kask, kbid) 149 | if profit >= 0 and profit >= best_profit: 150 | best_profit = profit 151 | best_volume = volume 152 | best_i, best_j = (i, j) 153 | best_w_buyprice, best_w_sellprice = ( 154 | w_buyprice, w_sellprice) 155 | return best_profit, best_volume, \ 156 | self.depths[kask]["asks"][best_i]["price"], \ 157 | self.depths[kbid]["bids"][best_j]["price"], \ 158 | best_w_buyprice, best_w_sellprice 159 | 160 | def arbitrage_opportunity(self, kask, ask, kbid, bid): 161 | perc = (bid["price"] - ask["price"]) / bid["price"] * 100 162 | profit, volume, buyprice, sellprice, weighted_buyprice,\ 163 | weighted_sellprice = self.arbitrage_depth_opportunity(kask, kbid) 164 | if volume == 0 or buyprice == 0: 165 | return 166 | perc2 = (weighted_sellprice-weighted_buyprice)/buyprice * 100 167 | for observer in self.observers: 168 | observer.opportunity( 169 | profit, volume, buyprice, kask, sellprice, kbid, 170 | perc2, weighted_buyprice, weighted_sellprice) 171 | 172 | def __get_market_depth(self, market, depths): 173 | depths[market.name] = market.get_depth() 174 | 175 | def update_depths(self): 176 | depths = {} 177 | futures = [] 178 | for market in self.markets: 179 | futures.append(self.threadpool.submit(self.__get_market_depth, 180 | market, depths)) 181 | wait(futures, timeout=20) 182 | return depths 183 | 184 | def tickers(self): 185 | for market in self.markets: 186 | logging.verbose("ticker: " + market.name + " - " + str( 187 | market.get_ticker())) 188 | 189 | def replay_history(self, directory): 190 | import os 191 | import json 192 | import pprint 193 | files = os.listdir(directory) 194 | files.sort() 195 | for f in files: 196 | depths = json.load(open(directory + '/' + f, 'r')) 197 | self.depths = {} 198 | for market in self.market_names: 199 | if market in depths: 200 | self.depths[market] = depths[market] 201 | self.tick() 202 | 203 | def tick(self): 204 | for observer in self.observers: 205 | observer.begin_opportunity_finder(self.depths) 206 | 207 | for kmarket1 in self.depths: 208 | for kmarket2 in self.depths: 209 | if kmarket1 == kmarket2: # same market 210 | continue 211 | market1 = self.depths[kmarket1] 212 | market2 = self.depths[kmarket2] 213 | if market1["asks"] and market2["bids"] \ 214 | and len(market1["asks"]) > 0 and len(market2["bids"]) > 0: 215 | if float(market1["asks"][0]['price']) \ 216 | < float(market2["bids"][0]['price']): 217 | self.arbitrage_opportunity(kmarket1, market1["asks"][0], 218 | kmarket2, market2["bids"][0]) 219 | 220 | for observer in self.observers: 221 | observer.end_opportunity_finder() 222 | 223 | def terminate(self): 224 | for observer in self.observers: 225 | observer.terminate() 226 | 227 | for market in self.markets: 228 | market.terminate() 229 | 230 | def loop(self): 231 | # 232 | signal.signal(signal.SIGINT, sigint_handler) 233 | 234 | #以下那句在windows python2.4不通过,但在freebsd下通过 235 | signal.signal(signal.SIGHUP, sigint_handler) 236 | 237 | signal.signal(signal.SIGTERM, sigint_handler) 238 | 239 | while True: 240 | self.depths = self.update_depths() 241 | # print(self.depths) 242 | self.tickers() 243 | self.tick() 244 | time.sleep(config.refresh_rate) 245 | 246 | if is_sigint_up: 247 | # 中断时需要处理的代码 248 | self.terminate() 249 | print ("Exit") 250 | break 251 | -------------------------------------------------------------------------------- /arbitrage/bitstar_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import decimal 4 | 5 | from lib.bitstar_sdk import ApiClient 6 | import config 7 | 8 | API_KEY = config.BITSTAR_API_KEY 9 | API_SECRET = config.BITSTAR_SECRET_TOKEN 10 | 11 | def main(): 12 | client = ApiClient(API_KEY, API_SECRET) 13 | 14 | # 用户主资产信息 15 | # currency 数字货币币种代号 btc  比特币 eth  以太坊 16 | main_account = client.get_main_account('btc') 17 | print(main_account) 18 | 19 | # 用户子账号信息(合约资产) 20 | # contractCode 合约类型代号 swap-btc  比特币现货合约 swap-eth  以太坊现货合约 21 | sub_account = client.get_sub_account('swap-btc') 22 | print(sub_account) 23 | 24 | # 主资产向子账号转入 25 | transfer_sub = client.transfer_to_sub('swap-btc', decimal.Decimal('0.02')) 26 | print(transfer_sub) 27 | 28 | # 子账号向主资产转出 29 | transfer_main = client.transfer_to_main('swap-btc', decimal.Decimal('0.01')) 30 | print(transfer_main) 31 | 32 | # 下单 33 | # businessType 业务类型代码 swap-btc-cny  比特币对人民币现货合约 swap-eth-cny  以太坊对人民币现货合约 34 | # tradeType 交易类型 1 开多  2 开空  3 平多  4 平空 35 | # price 只保留一位有效小数 36 | # amount 数量是人民币的数量,100的整数倍 37 | trade = client.trade('swap-btc-cny', 2, decimal.Decimal('18300.1'), 100) 38 | print(trade) 39 | 40 | order_id = trade['orderid'] 41 | 42 | # 撤销订单 43 | cancel = client.cancel('swap-btc-cny', order_id) 44 | print(cancel) 45 | 46 | # 查看单个订单信息 47 | order_info = client.order_info('swap-btc-cny', order_id) 48 | print(order_info) 49 | 50 | # 查看委托中订单信息 51 | orders_in = client.order_in_list('swap-btc-cny') 52 | print(orders_in) 53 | 54 | # 查看最近完成订单信息 55 | orders_over = client.order_over_list('swap-btc-cny') 56 | print(orders_over) 57 | 58 | # 查看持仓信息 59 | storeinfo = client.storeinfo('swap-btc-cny') 60 | print(storeinfo) 61 | 62 | publicinfo = client.publicinfo('swap-btc-cny') 63 | print(publicinfo) 64 | 65 | if __name__ == '__main__': 66 | main() 67 | -------------------------------------------------------------------------------- /arbitrage/config.py-example: -------------------------------------------------------------------------------- 1 | markets = [ 2 | # "BitfinexUSD", 3 | # "BitstampUSD", 4 | # "BTCCCNY", 5 | # "BtceEUR", 6 | # "BtceUSD", 7 | # "CampBXUSD", 8 | # "CoinbaseUSD", 9 | # "GeminiUSD", 10 | # "KrakenEUR", 11 | # "KrakenUSD", 12 | # "OKCoinCNY", 13 | "HaobtcCNY", 14 | # "HuobiCNY", 15 | # "PaymiumEUR", 16 | ] 17 | 18 | # observers if any 19 | # ["Logger", "DetailedLogger", "TraderBot", "TraderBotSim", "HistoryDumper", "Emailer", "SpecializedTraderBot"] 20 | observers = ["DetailedLogger", "TraderBotSim"] 21 | 22 | market_expiration_time = 120 # in seconds: 2 minutes 23 | 24 | refresh_rate = 20 25 | 26 | trade_wait = 10 27 | 28 | 29 | MAKER_TRADE_ENABLE = False 30 | TAKER_TRADE_ENABLE = True 31 | # maker 32 | MAKER_MAX_VOLUME = 30 33 | MAKER_MIN_VOLUME = 1 34 | MAKER_BUY_QUEUE = 3 35 | MAKER_BUY_STAGE = 1 36 | MAKER_SELL_QUEUE = 3 37 | MAKER_SELL_STAGE = 2 38 | 39 | TAKER_MAX_VOLUME = 1 40 | TAKER_MIN_VOLUME = 0.01 41 | 42 | bid_fee_rate = 0.001 43 | ask_fee_rate = 0.001 44 | bid_price_risk = 0 45 | ask_price_risk = 0 46 | 47 | 48 | #hedger 49 | balance_margin = 0.1 # 10% 50 | 51 | profit_thresh = 3 # in CNY 52 | perc_thresh = 0.01 # in 0.01% 53 | max_tx_volume = 3 # in BTC 54 | min_tx_volume = 0.5 # in BTC 55 | 56 | reverse_profit_thresh = 1 57 | reverse_perc_thresh = 0.01 58 | reverse_max_tx_volume = 1 # in BTC 59 | 60 | stage0_percent=0.1 61 | stage1_percent=0.2 62 | 63 | ARBITRAGER_BUY_QUEUE = 5 64 | ARBITRAGER_SELL_QUEUE = 5 65 | 66 | arbitrage_cancel_price_diff = 2 67 | 68 | broker_min_amount = 0.01 69 | 70 | #stata 71 | cny_init = 60000000000 72 | btc_init = 1200000 73 | price_init = 4450 74 | 75 | #### Emailer Observer Config 76 | send_trade_mail = False 77 | 78 | EMAIL_HOST = 'mail.FIXME.com' 79 | EMAIL_HOST_USER = 'FIXME@FIXME.com' 80 | EMAIL_HOST_PASSWORD = 'FIXME' 81 | EMAIL_USE_TLS = True 82 | 83 | EMAIL_RECEIVER = ['FIXME@FIXME.com'] 84 | 85 | 86 | #### XMPP Observer 87 | xmpp_jid = "FROM@jabber.org" 88 | xmpp_password = "FIXME" 89 | xmpp_to = "TO@jabber.org" 90 | 91 | # broker thrift server 92 | BROKER_HOST = "127.0.0.1" 93 | BROKER_PORT = 18030 94 | 95 | #### Trader Bot Config 96 | # Access to Private APIs 97 | 98 | paymium_username = "FIXME" 99 | paymium_password = "FIXME" 100 | paymium_address = "FIXME" # to deposit btc from markets / wallets 101 | 102 | bitstamp_username = "FIXME" 103 | bitstamp_password = "FIXME" 104 | 105 | HUOBI_API_KEY = '' 106 | HUOBI_SECRET_TOKEN = '' 107 | 108 | OKCOIN_API_KEY = '' 109 | OKCOIN_SECRET_TOKEN = '' 110 | 111 | HAOBTC_API_KEY = '' 112 | HAOBTC_SECRET_TOKEN = '' 113 | 114 | BITSTAR_API_KEY = '' 115 | BITSTAR_SECRET_TOKEN = '' 116 | 117 | Bitfinex_API_KEY = '' 118 | Bitfinex_SECRET_TOKEN = '' 119 | 120 | Bittrex_API_KEY = '' 121 | Bittrex_SECRET_TOKEN = '' 122 | 123 | 124 | SUPPORT_ZMQ = True 125 | ZMQ_HOST = "127.0.0.1" 126 | ZMQ_PORT = 18031 127 | 128 | SUPPORT_WEBSOCKET = False 129 | WEBSOCKET_HOST = 'http://localhost' 130 | WEBSOCKET_PORT = 13001 131 | 132 | ENV = 'local' 133 | 134 | try: 135 | from config_local import * 136 | except ImportError: 137 | pass 138 | -------------------------------------------------------------------------------- /arbitrage/fiatconverter.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013, Maxime Biais 2 | # Copyright (C) 2016, Phil Song 3 | 4 | import urllib.request 5 | import sys 6 | import json 7 | import logging 8 | import time 9 | 10 | 11 | class FiatConverter: 12 | __shared_state = {} 13 | rate_exchange_url = "http://rate-exchange.appspot.com/currency?from=%s&to=%s" 14 | rate_exchange_url_yahoo = "http://download.finance.yahoo.com/d/quotes.csv?s=%s%s=X&f=sl1d1&e=.csv" 15 | 16 | def __init__(self): 17 | """USD is used as pivot""" 18 | self.__dict__ = self.__shared_state 19 | self.rates = { 20 | "USD": 1, 21 | "EUR": 0.8825, 22 | "CNY": 6.5184, 23 | } 24 | self.update_delay = 60 * 60 # every hour 25 | self.last_update = 0 26 | self.bank_fee = 0.007 # FIXME: bank fee 27 | 28 | def get_currency_pair(self, code_from, code_to): 29 | url = self.rate_exchange_url % (code_from, code_to) 30 | res = urllib.request.urlopen(url) 31 | data = json.loads(res.read().decode('utf8')) 32 | rate = 0 33 | if "rate" in data: 34 | rate = float(data["rate"]) * (1.0 - self.bank_fee) 35 | else: 36 | logging.error("Can't update fiat conversion rate: %s", url) 37 | return rate 38 | 39 | def get_currency_pair_yahoo(self, code_from, code_to): 40 | url = self.rate_exchange_url_yahoo % (code_from, code_to) 41 | res = urllib.request.urlopen(url) 42 | data = res.read().decode('utf8').split(",")[1] 43 | rate = float(data) * (1.0 - self.bank_fee) 44 | return rate 45 | 46 | def update_currency_pair(self, code_to): 47 | if code_to == "USD": 48 | return 49 | code_from = "USD" 50 | try: 51 | rate = self.get_currency_pair(code_from, code_to) 52 | except urllib.error.HTTPError: 53 | rate = self.get_currency_pair_yahoo(code_from, code_to) 54 | if rate: 55 | self.rates[code_to] = rate 56 | 57 | def update(self): 58 | #CLOSE THE CONVERT 59 | return 60 | 61 | timediff = time.time() - self.last_update 62 | if timediff < self.update_delay: 63 | return 64 | self.last_update = time.time() 65 | for currency in self.rates: 66 | self.update_currency_pair(currency) 67 | 68 | def convert(self, price, code_from, code_to): 69 | if code_from == code_to: 70 | return price 71 | 72 | self.update() 73 | rate_from = self.rates[code_from] 74 | rate_to = self.rates[code_to] 75 | return price / rate_from * rate_to 76 | 77 | 78 | if __name__ == "__main__": 79 | fc = FiatConverter() 80 | print(fc.convert(12., "USD", "EUR")) 81 | print(fc.convert(12., "EUR", "USD")) 82 | print(fc.convert(1., "USD", "CNY")) 83 | print(fc.convert(1., "CNY", "USD")) 84 | print(fc.convert(1., "EUR", "CNY")) 85 | 86 | print(fc.rates) 87 | -------------------------------------------------------------------------------- /arbitrage/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artooze/crypto-arbitrager/49aaac9255953cb6c36ab16b09d024afd0dd0935/arbitrage/lib/__init__.py -------------------------------------------------------------------------------- /arbitrage/lib/bitstar_sdk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import decimal 4 | import hashlib 5 | import json 6 | 7 | import requests 8 | 9 | 10 | class Dict(dict): 11 | def __init__(self, **kw): 12 | super().__init__(**kw) 13 | 14 | def __getattr__(self, key): 15 | try: 16 | return self[key] 17 | except KeyError: 18 | raise AttributeError(r"'Dict' object has no attribute '%s'" % key) 19 | 20 | def __setattr__(self, key, value): 21 | self[key] = value 22 | 23 | 24 | def _toDict(d): 25 | return Dict(**d) 26 | 27 | 28 | class ApiClient(object): 29 | # timeout in 5 seconds: 30 | timeout = 5 31 | api_url = 'https://www.bitstar.com' 32 | api_version = '/api/v1' 33 | 34 | def __init__(self, appKey, appSecret): 35 | self._accessKeyId = appKey 36 | self._accessKeySecret = appSecret # change to bytes 37 | 38 | def get_main_account(self, currency: str): 39 | """ 40 | 用户主资产信息 41 | https://github.com/bitstarcom/BitStar-API/wiki/%E7%94%A8%E6%88%B7%E4%B8%BB%E8%B5%84%E4%BA%A7%E4%BF%A1%E6%81%AF 42 | :param currency: 43 | :return: 44 | """ 45 | uri = '/fund/mainAccount/%s' % (currency,) 46 | return self._call(uri) 47 | 48 | def get_sub_account(self, contract_code: str): 49 | """ 50 | 用户子账号信息(合约资产) 51 | https://github.com/bitstarcom/BitStar-API/wiki/%E7%94%A8%E6%88%B7%E5%AD%90%E8%B4%A6%E5%8F%B7%E4%BF%A1%E6%81%AF%EF%BC%88%E5%90%88%E7%BA%A6%E8%B5%84%E4%BA%A7%EF%BC%89 52 | :param contract_code: 53 | :return: 54 | """ 55 | uri = '/fund/subAccount/%s' % (contract_code,) 56 | return self._call(uri) 57 | 58 | def transfer_to_sub(self, contract_code: str, amount: decimal.Decimal): 59 | """ 60 | 主资产向子账号转入 61 | https://github.com/bitstarcom/BitStar-API/wiki/%E4%B8%BB%E8%B5%84%E4%BA%A7%E5%90%91%E5%AD%90%E8%B4%A6%E5%8F%B7%E8%BD%AC%E5%85%A5 62 | :param contract_code: 63 | :param amount: 64 | :return: 65 | """ 66 | uri = '/fund/transferToSub/%s/%s' % (contract_code, amount) 67 | return self._call(uri) 68 | 69 | def transfer_to_main(self, contract_code: str, amount: decimal.Decimal): 70 | """ 71 | 子账号向主资产转出 72 | https://github.com/bitstarcom/BitStar-API/wiki/%E5%AD%90%E8%B4%A6%E5%8F%B7%E5%90%91%E4%B8%BB%E8%B5%84%E4%BA%A7%E8%BD%AC%E5%87%BA 73 | :param contract_code: 74 | :param amount: 75 | :return: 76 | """ 77 | uri = '/fund/transferToMain/%s/%s' % (contract_code, amount) 78 | return self._call(uri) 79 | 80 | def trade(self, business_type: str, trade_type: int, price: decimal.Decimal, amount: int): 81 | """ 82 | 下单 83 | https://github.com/bitstarcom/BitStar-API/wiki/%E4%B8%8B%E5%8D%95 84 | :param business_type: 85 | :param trade_type: 86 | :param price: 87 | :param amount: 88 | :return: 89 | """ 90 | uri = '/trade/order/%s/%s/%s/%s' % (business_type, trade_type, price, amount) 91 | return self._call(uri) 92 | 93 | def cancel(self, business_type: str, order_id: int): 94 | """ 95 | 撤销订单 96 | https://github.com/bitstarcom/BitStar-API/wiki/%E6%92%A4%E9%94%80%E8%AE%A2%E5%8D%95 97 | :param order_id: 98 | :return: 99 | """ 100 | uri = '/trade/cancel/%s/%s' % (business_type, order_id) 101 | return self._call(uri) 102 | 103 | def order_info(self, business_type: str, order_id: int): 104 | """ 105 | 查看单个订单信息 106 | https://github.com/bitstarcom/BitStar-API/wiki/%E6%9F%A5%E7%9C%8B%E5%8D%95%E4%B8%AA%E8%AE%A2%E5%8D%95%E4%BF%A1%E6%81%AF 107 | :param business_type: 108 | :param order_id: 109 | :return: 110 | """ 111 | uri = '/trade/orderinfo/%s/%s' % (business_type, order_id) 112 | return self._call(uri) 113 | 114 | def order_in_list(self, busiess_type: str): 115 | """ 116 | 查看委托中订单信息 117 | https://github.com/bitstarcom/BitStar-API/wiki/%E6%9F%A5%E7%9C%8B%E5%A7%94%E6%89%98%E4%B8%AD%E8%AE%A2%E5%8D%95%E4%BF%A1%E6%81%AF 118 | :param busiess_type: 119 | :return: 120 | """ 121 | uri = '/trade/orders_in/%s' % (busiess_type,) 122 | return self._call(uri) 123 | 124 | def order_over_list(self, busiess_type: str): 125 | """ 126 | 查看最近完成订单信息 127 | https://github.com/bitstarcom/BitStar-API/wiki/%E6%9F%A5%E7%9C%8B%E6%9C%80%E8%BF%91%E5%AE%8C%E6%88%90%E8%AE%A2%E5%8D%95%E4%BF%A1%E6%81%AF 128 | :param busiess_type: 129 | :return: 130 | """ 131 | uri = '/trade/orders_over/%s' % (busiess_type,) 132 | return self._call(uri) 133 | 134 | def storeinfo(self, busiess_type: str): 135 | """ 136 | 查看持仓信息 137 | https://github.com/bitstarcom/BitStar-API/wiki/%E6%9F%A5%E7%9C%8B%E6%8C%81%E4%BB%93%E4%BF%A1%E6%81%AF 138 | :param busiess_type: 139 | :return: 140 | """ 141 | uri = '/trade/storeinfo/%s' % (busiess_type,) 142 | return self._call(uri) 143 | 144 | def publicinfo(self, busiess_type: str): 145 | """ 146 | 查看持仓信息 147 | https://github.com/bitstarcom/BitStar-API/wiki/%E6%9F%A5%E7%9C%8B%E6%8C%81%E4%BB%93%E4%BF%A1%E6%81%AF 148 | :param busiess_type: 149 | :return: 150 | """ 151 | uri = '/market/publickinfo/%s' % (busiess_type,) 152 | return self._get(uri) 153 | 154 | def _call(self, uri): 155 | sign_uri = self.api_version + uri + '/' + self._accessKeySecret 156 | md5 = hashlib.md5() 157 | md5.update(sign_uri.encode('utf-8')) 158 | sign = md5.hexdigest() 159 | params = { 160 | 'accessKey': self._accessKeyId, 161 | 'signData': sign 162 | } 163 | req_url = self.api_url + self.api_version + uri 164 | resp = requests.post(url=req_url, data=params, timeout=self.timeout) 165 | return self._parse(resp.text) 166 | 167 | def _parse(self, text): 168 | result = json.loads(text, object_hook=_toDict) 169 | if 'error' not in result: 170 | return result 171 | raise ApiError('%s' % (text)) 172 | 173 | def _get(self, uri, params = None): 174 | req_url = self.api_url + self.api_version + uri 175 | resp = requests.get(url=req_url, params=params, timeout=self.timeout) 176 | return self._parse(resp.text) 177 | 178 | class ApiError(BaseException): 179 | pass 180 | -------------------------------------------------------------------------------- /arbitrage/lib/broker.thrift: -------------------------------------------------------------------------------- 1 | 2 | namespace go trade_service 3 | namespace py trade_service 4 | 5 | const string EX_INTERNAL_ERROR = "trader internal error"; 6 | const string EX_PRICE_OUT_OF_SCOPE = "price is out of scope"; 7 | const string EX_NO_USABLE_FUND = "no usable exchange fund by now"; 8 | const string EX_NO_USABLE_DEPTH = "no usable exchange depth by now"; 9 | const string EX_TRADE_QUEUE_FULL = "trade queue is full"; 10 | const string EX_DEPTH_INSUFFICIENT = "exchange depth is insufficient"; 11 | const string EX_PRICE_NOT_SYNC = "price is not in tickers"; 12 | const string EX_EXIST_ERROR_ORDERS = "exist error status orders"; 13 | 14 | 15 | exception TradeException { 16 | 1: string reason, 17 | } 18 | 19 | struct ExchangeStatus { 20 | 1: bool canbuy, 21 | 2: bool cansell, 22 | } 23 | 24 | struct ExchangeConfig{ 25 | 1: string exchange, 26 | 2: string access_key, 27 | 3: string secret_key, 28 | } 29 | 30 | struct AmountConfig{ 31 | 1: double max_cny, 32 | 2: double max_btc, 33 | } 34 | 35 | struct Account { 36 | 1: string exchange , 37 | 2: double available_cny , 38 | 3: double available_btc , 39 | 4: double frozen_cny , 40 | 5: double frozen_btc , 41 | 6: bool pause_trade, 42 | } 43 | 44 | struct Ticker { 45 | 1: double ask, 46 | 2: double bid, 47 | } 48 | 49 | struct Trade{ 50 | 1: string client_id, 51 | 2: double amount, 52 | 3: double price, 53 | } 54 | 55 | 56 | enum TradeType{ 57 | BUY, 58 | SELL, 59 | } 60 | 61 | enum OrderStatus{ 62 | TIME_WEIGHTED, 63 | SPLIT, 64 | READY, 65 | ORDERED, 66 | SUCCESS, 67 | ERROR, 68 | CANCELED, 69 | MATCH 70 | } 71 | 72 | 73 | struct TradeOrder { 74 | 1:i64 id 75 | 2:i64 site_order_id 76 | 3:string exchange 77 | 4:double price 78 | 5:TradeType trade_type 79 | 6:OrderStatus order_status, 80 | 7:double estimate_cny 81 | 8:double estimate_btc 82 | 9:double estimate_price 83 | 10:double deal_cny 84 | 11:double deal_btc 85 | 12:double deal_price 86 | 13:double price_margin 87 | 14:string order_id 88 | 15:string created 89 | 16:string update_at 90 | 17:i64 try_times 91 | 18:string info 92 | 19:string memo 93 | 20:i64 match_id 94 | } 95 | 96 | service TradeService { 97 | oneway void ping(), 98 | oneway void config_keys(1: list exchange_configs), 99 | oneway void config_amount(1: AmountConfig amount_config), 100 | ExchangeStatus get_exchange_status(), 101 | void check_price(1:double price, 2:TradeType trade_type) throws (1:TradeException tradeException), 102 | void buy(1:Trade trade) throws (1:TradeException tradeException), 103 | void sell(1:Trade trade) throws (1:TradeException tradeException), 104 | Ticker get_ticker(), 105 | list get_account(), 106 | void get_alert_orders() throws (1:TradeException tradeException) //client will retry every minute, then send email/sms 107 | } 108 | //save all depth 109 | -------------------------------------------------------------------------------- /arbitrage/lib/broker_api.py: -------------------------------------------------------------------------------- 1 | import thriftpy 2 | broker_thrift = thriftpy.load("arbitrage/lib/broker.thrift", module_name="broker_thrift") 3 | 4 | from thriftpy.rpc import make_client 5 | from thriftpy.protocol.binary import TBinaryProtocolFactory 6 | from thriftpy.transport.framed import TFramedTransportFactory 7 | 8 | import config 9 | import logging 10 | import traceback 11 | 12 | client = None 13 | 14 | def init_broker(): 15 | try: 16 | global client 17 | client = make_client(broker_thrift.TradeService, config.BROKER_HOST, config.BROKER_PORT, 18 | proto_factory=TBinaryProtocolFactory(), 19 | trans_factory=TFramedTransportFactory(), 20 | timeout=60000) 21 | except Exception as e: 22 | logging.warn("make_client exception") 23 | traceback.print_exc() 24 | 25 | def exchange_ping(): 26 | client.ping() 27 | return 28 | 29 | def exchange_get_status(): 30 | logging.debug('exchange_get_status') 31 | exchange_status = client.get_exchange_status() 32 | logging.debug('exchange_get_status %s', exchange_status) 33 | 34 | return exchange_status 35 | 36 | def exchange_check_price(price, trade_type): 37 | logging.debug('check_price %s %s', price, trade_type) 38 | client.check_price(price, trade_type) 39 | logging.debug("exchange_check_price-> end") 40 | 41 | return 42 | 43 | def exchange_buy(client_id, btc, price): 44 | logging.debug('exchange_buy %s %s %s', client_id, btc, price) 45 | buyOrder = broker_thrift.Trade(str(client_id), btc, price) 46 | client.buy(buyOrder) 47 | logging.debug("exchange_buy-> end") 48 | 49 | return 50 | 51 | def exchange_sell(client_id, btc, price): 52 | logging.debug('exchange_sell %s %s %s', client_id, btc, price) 53 | sellOrder = broker_thrift.Trade(str(client_id), btc, price) 54 | client.sell(sellOrder) 55 | logging.debug("exchange_sell-> end") 56 | 57 | return 58 | 59 | def exchange_get_ticker(): 60 | logging.debug('get_ticker') 61 | ticker= client.get_ticker() 62 | logging.debug("get_ticker-> %s",ticker) 63 | 64 | return ticker 65 | 66 | def exchange_get_account(): 67 | logging.debug("get_account") 68 | accounts= client.get_account() 69 | logging.debug("get_account-> %s",accounts) 70 | 71 | return accounts 72 | 73 | def exchange_get_alert_orders(): 74 | logging.debug("get_alert_orders") 75 | alert_orders= client.get_alert_orders() 76 | logging.debug("get_alert_orders-> %s",alert_orders) 77 | 78 | return alert_orders 79 | 80 | 81 | def exchange_config_keys(exchange_configs): 82 | logging.debug("config_keys->begin") 83 | client.config_keys(exchange_configs) 84 | logging.debug("config_keys->end") 85 | 86 | return 87 | 88 | def exchange_config_amount(amount_config): 89 | logging.debug("config_amount->%s", amount_config) 90 | client.config_amount(amount_config) 91 | logging.debug("config_amount->end") 92 | 93 | return 94 | 95 | -------------------------------------------------------------------------------- /arbitrage/lib/exchange.py: -------------------------------------------------------------------------------- 1 | import math 2 | import time 3 | import datetime 4 | import requests 5 | import re 6 | import hashlib 7 | import logging 8 | import sys 9 | import os 10 | from .helpers import * 11 | from .settings import * 12 | 13 | 14 | class exchange: 15 | 16 | def __init__(self, url, apiKey, secretToken, role = 'default'): 17 | """ 18 | Role : liquidity , arbitrage , soleTrade 19 | 20 | """ 21 | self.url = url 22 | self.apikey = apiKey 23 | self.secretToken = secretToken 24 | self.role = role 25 | 26 | def market(self): 27 | return self.role 28 | 29 | def buy(self, amount, price,tradePassword=None,tradeid=None): 30 | if self.role == 'haobtc' or self.role == 'default': 31 | payload = {'amount':amount,'price':price,'api_key':self.apikey,'secret_key':self.secretToken,'type':'buy'} 32 | payload = tradeLoad(payload, self.secretToken , self.role) 33 | return requestPost(self.url['trade'] , payload) 34 | 35 | if self.role == 'okcoin': 36 | body = requestBody(self.url['trade'], self.url['host']) 37 | params = { 38 | 'api_key':self.apikey, 39 | 'symbol':'btc_cny', 40 | 'type':'buy' 41 | } 42 | if price: 43 | params['price'] = price 44 | if amount: 45 | params['amount'] = amount 46 | 47 | params['sign'] = buildSign(params,self.secretToken, self.role) 48 | r = httpPost(self.url['host'], body, params) 49 | if r: 50 | return json.loads(r) 51 | else: 52 | return None 53 | 54 | if self.role == 'huobi': 55 | timestamp = int(time.time()) 56 | params = {"access_key": self.apikey, 57 | "secret_key": self.secretToken, 58 | "created": timestamp, 59 | "price":price, 60 | "coin_type":1, 61 | "amount":amount, 62 | "method":self.url['buy']} 63 | sign=signature(params) 64 | params['sign']=sign 65 | del params['secret_key'] 66 | if tradePassword: 67 | params['trade_password']=tradePassword 68 | if tradeid: 69 | params['trade_id']=tradeid 70 | 71 | payload = urllib.parse.urlencode(params) 72 | r = requests.post("http://"+self.url['host'], params=payload) 73 | if r.status_code == 200: 74 | data = r.json() 75 | return data 76 | else: 77 | return None 78 | 79 | def bidMakerOnly(self, amount, price): 80 | if self.role == 'haobtc' or self.role == 'default': 81 | payload = {'amount':amount,'price':price,'api_key':self.apikey,'secret_key':self.secretToken,'type':'buy_maker_only'} 82 | payload = tradeLoad(payload, self.secretToken , self.role) 83 | return requestPost(self.url['trade'] , payload) 84 | 85 | def askMakerOnly(self, amount, price): 86 | if self.role == 'haobtc' or self.role == 'default': 87 | payload = {'amount':amount,'price':price,'api_key':self.apikey,'secret_key':self.secretToken,'type':'sell_maker_only'} 88 | payload = tradeLoad(payload, self.secretToken , self.role) 89 | return requestPost(self.url['trade'] , payload) 90 | 91 | def sell(self, amount, price, tradePassword=None, tradeid=None): 92 | if self.role == 'haobtc' or self.role == 'default': 93 | payload = {'amount':amount,'price':price,'api_key':self.apikey,'secret_key':self.secretToken,'type':'sell'} 94 | payload = tradeLoad(payload, self.secretToken , self.role) 95 | return requestPost(self.url['trade'], payload) 96 | 97 | 98 | if self.role == 'okcoin': 99 | body = requestBody(self.url['trade'], self.url['host']) 100 | params = { 101 | 'api_key':self.apikey, 102 | 'symbol':'btc_cny', 103 | 'type':'sell' 104 | } 105 | if price: 106 | params['price'] = price 107 | if amount: 108 | params['amount'] = amount 109 | 110 | params['sign'] = buildSign(params,self.secretToken, self.role) 111 | r = httpPost(self.url['host'], body, params) 112 | if r: 113 | return json.loads(r) 114 | else: 115 | return None 116 | 117 | if self.role == 'huobi': 118 | timestamp = int(time.time()) 119 | params = {"access_key": self.apikey, 120 | "secret_key": self.secretToken, 121 | "created": timestamp, 122 | "price":price, 123 | "coin_type":1, 124 | "amount":amount, 125 | "method":self.url['sell']} 126 | sign=signature(params) 127 | params['sign']=sign 128 | del params['secret_key'] 129 | if tradePassword: 130 | params['trade_password']=tradePassword 131 | if tradeid: 132 | params['trade_id']=tradeid 133 | 134 | payload = urllib.parse.urlencode(params) 135 | r = requests.post("http://"+self.url['host'], params=payload) 136 | if r and r.status_code == 200: 137 | data = r.json() 138 | return data 139 | else: 140 | return None 141 | 142 | 143 | def marketBuy(self, amount): 144 | if self.role == 'haobtc' or self.role == 'default': 145 | payload = {'amount':amount,'api_key':self.apikey,'secret_key':self.secretToken,'type':'buy_market'} 146 | payload = tradeLoad(payload, self.secretToken , self.role) 147 | return requestPost(self.url['trade'] , payload) 148 | 149 | if self.role == 'okcoin': 150 | body = requestBody(self.url['trade'], self.url['host']) 151 | params = { 152 | 'api_key':self.apikey, 153 | 'symbol':'btc_cny', 154 | 'type':'buy_market' 155 | } 156 | if price: 157 | params['price'] = price 158 | if amount: 159 | params['amount'] = amount 160 | 161 | params['sign'] = buildSign(params,self.secretToken, self.role) 162 | r = httpPost(self.url['host'], body, params) 163 | if r: 164 | return json.loads(r) 165 | else: 166 | return None 167 | 168 | if self.role == 'huobi': 169 | timestamp = int(time.time()) 170 | params = {"access_key": self.apikey, 171 | "secret_key": self.secretToken, 172 | "created": timestamp, 173 | "coin_type":1, 174 | "amount":amount, 175 | "method":self.url['buy_market'], 176 | } 177 | sign=signature(params) 178 | params['sign']=sign 179 | del params['secret_key'] 180 | payload = urllib.parse.urlencode(params) 181 | r = requests.post("http://"+self.url['host'], params=payload) 182 | if r.status_code == 200: 183 | data = r.json() 184 | return data 185 | else: 186 | return None 187 | 188 | def marketSell(self, amount): 189 | if self.role == 'haobtc' or self.role == 'default': 190 | payload = {'amount':amount,'api_key':self.apikey,'secret_key':self.secretToken,'type':'sell_market'} 191 | payload = tradeLoad(payload, self.secretToken , self.role) 192 | return requestPost(self.url['trade'] , payload) 193 | 194 | if self.role == 'okcoin': 195 | body = requestBody(self.url['trade'], self.url['host']) 196 | params = { 197 | 'api_key':self.apikey, 198 | 'symbol':'btc_cny', 199 | 'type':'buy_market' 200 | } 201 | if price: 202 | params['price'] = price 203 | if amount: 204 | params['amount'] = amount 205 | 206 | params['sign'] = buildSign(params,self.secretToken, self.role) 207 | r = httpPost(self.url['host'], body, params) 208 | if r: 209 | return json.loads(r) 210 | else: 211 | return None 212 | 213 | if self.role == 'huobi': 214 | timestamp = int(time.time()) 215 | params = {"access_key": self.apikey, 216 | "secret_key": self.secretToken, 217 | "created": timestamp, 218 | "coin_type":1, 219 | "amount":amount, 220 | "method":self.url['sell_market'], 221 | } 222 | sign=signature(params) 223 | params['sign']=sign 224 | del params['secret_key'] 225 | payload = urllib.parse.urlencode(params) 226 | r = requests.post("http://"+self.url['host'], params=payload) 227 | if r.status_code == 200: 228 | data = r.json() 229 | return data 230 | else: 231 | return None 232 | 233 | 234 | def cancel(self,id): 235 | if self.role == 'haobtc' or self.role == 'default': 236 | payload = {'api_key':self.apikey, "order_id":id} 237 | payload = tradeLoad(payload, self.secretToken , self.role) 238 | return requestPost(self.url['cancel_order'] , payload) 239 | 240 | if self.role == 'okcoin': 241 | body = requestBody(self.url['cancel_order'], self.url['host']) 242 | params = { 243 | 'api_key':self.apikey, 244 | 'symbol':'btc_cny', 245 | 'order_id':id 246 | } 247 | 248 | params['sign'] = buildSign(params,self.secretToken, self.role) 249 | r = httpPost(self.url['host'], body, params) 250 | if r: 251 | return json.loads(r) 252 | else: 253 | return None 254 | 255 | if self.role == 'huobi': 256 | timestamp = int(time.time()) 257 | params = {"access_key": self.apikey, 258 | "secret_key": self.secretToken, 259 | "created": timestamp, 260 | "coin_type":1, 261 | "method":self.url['cancel_order'], 262 | "id":id} 263 | sign=signature(params) 264 | params['sign']=sign 265 | del params['secret_key'] 266 | payload = urllib.parse.urlencode(params) 267 | r = requests.post("http://"+self.url['host'], params=payload) 268 | if r.status_code == 200: 269 | data = r.json() 270 | return data 271 | else: 272 | return None 273 | 274 | def cancelAll(self): 275 | if self.role == 'haobtc' or self.role == 'default': 276 | payload = {'api_key':self.apikey} 277 | payload = tradeLoad(payload, self.secretToken , self.role) 278 | return requestPost(self.url['cancel_all'] , payload) 279 | 280 | if self.role == '': 281 | return 282 | 283 | if self.role == '': 284 | return 285 | 286 | 287 | def orderInfo(self, id): 288 | if self.role == 'haobtc' or self.role == 'default': 289 | payload = {'api_key':self.apikey, "order_id":id} 290 | payload = tradeLoad(payload, self.secretToken , self.role) 291 | return requestPost(self.url['order_info'] , payload) 292 | 293 | if self.role == 'okcoin': 294 | body = requestBody(self.url['order_info'], self.url['host']) 295 | params = { 296 | 'api_key':self.apikey, 297 | 'symbol':'btc_cny', 298 | 'order_id':id 299 | } 300 | 301 | params['sign'] = buildSign(params,self.secretToken, self.role) 302 | r = httpPost(self.url['host'], body, params) 303 | if r: 304 | return json.loads(r) 305 | else: 306 | return None 307 | 308 | if self.role == 'huobi': 309 | timestamp = int(time.time()) 310 | params = {"access_key": self.apikey, 311 | "secret_key": self.secretToken, 312 | "created": timestamp, 313 | "coin_type":1, 314 | "method":self.url['order_info'], 315 | "id":id} 316 | sign=signature(params) 317 | params['sign']=sign 318 | del params['secret_key'] 319 | payload = urllib.parse.urlencode(params) 320 | r = requests.post("http://"+self.url['host'], params=payload) 321 | if r.status_code == 200: 322 | data = r.json() 323 | return data 324 | else: 325 | return None 326 | 327 | def ordersInfo(self,id=''): 328 | if self.role == 'haobtc' or self.role == 'default': 329 | payload = {'api_key':self.apikey} 330 | payload = tradeLoad(payload, self.secretToken , self.role) 331 | return requestPost(self.url['orders_info'] , payload) 332 | 333 | if self.role == 'okcoin': 334 | body = requestBody(self.url['orders_info'], self.url['host']) 335 | params = { 336 | 'api_key':self.apikey, 337 | 'symbol':'btc_cny', 338 | 'order_id':id, 339 | 'type':0 340 | } 341 | 342 | params['sign'] = buildSign(params,self.secretToken, self.role) 343 | r = httpPost(self.url['host'], body, params) 344 | if r: 345 | return json.loads(r) 346 | else: 347 | return None 348 | 349 | 350 | def orderHistory(self): 351 | if self.role == 'okcoin': 352 | body = requestBody(self.url['order_history'], self.url['host']) 353 | params = { 354 | 'api_key':self.apikey, 355 | 'current_page':1, 356 | 'page_length':199, 357 | 'status':0, 358 | 'symbol':'btc_cny' 359 | } 360 | 361 | params['sign'] = buildSign(params,self.secretToken, self.role) 362 | r = httpPost(self.url['host'], body, params) 363 | if r: 364 | return json.loads(r) 365 | else: 366 | return None 367 | 368 | def historyInfo(self,size): 369 | if self.role == 'haobtc' or self.role == 'default': 370 | payload = {'api_key':self.apikey,'size':size} 371 | payload = tradeLoad(payload, self.secretToken , self.role) 372 | return requestPost(self.url['history_info'] , payload) 373 | 374 | if self.role == '': 375 | return 376 | 377 | 378 | if self.role == '': 379 | return 380 | 381 | 382 | def accountInfo(self): 383 | if self.role == 'haobtc' or self.role == 'default': 384 | payload = {'api_key':self.apikey} 385 | payload = tradeLoad(payload, self.secretToken , self.role) 386 | return requestPost(self.url['account_info'], payload) 387 | 388 | if self.role == 'okcoin': 389 | params={} 390 | body = requestBody(self.url['userInfo'], self.url['host']) 391 | params['api_key'] = self.apikey 392 | params['sign'] = buildSign(params,self.secretToken,'okcoin') 393 | r = httpPost(self.url['host'], body, params) 394 | if r: 395 | return json.loads(r) 396 | else: 397 | return None 398 | 399 | if self.role == 'huobi': 400 | timestamp = int(time.time()) #python3 use int to replace int 401 | params = {"access_key": self.apikey,"secret_key": self.secretToken, "created": timestamp,"method":self.url['account_info']} 402 | sign=signature(params) 403 | params['sign']=sign 404 | del params['secret_key'] 405 | payload = urllib.parse.urlencode(params) 406 | r = requests.post("http://"+self.url['host'], params=payload) 407 | if r.status_code == 200: 408 | data = r.json() 409 | return data 410 | else: 411 | return None 412 | 413 | def ticker(self,symbol=''): 414 | 415 | if self.role == 'haobtc' or self.role == 'default': 416 | return requestGet(self.url['ticker']) 417 | 418 | if self.role == 'okcoin': 419 | body = requestBody(self.url['ticker'], self.url['host']) 420 | if symbol: 421 | params = 'symbol=%(symbol)s' %{'symbol':symbol} 422 | else: 423 | params = '' 424 | r = httpGet(self.url['host'], body, params) 425 | return r 426 | 427 | if self.role == 'huobi': 428 | return requestGet(self.url['ticker']) 429 | 430 | 431 | def depth(self, size=10, merge= 1, symbol=''): 432 | params='' 433 | if self.role == 'haobtc' or self.role == 'default': 434 | payload = {'api_key':self.apikey,'size':size} 435 | payload = tradeLoad(payload, self.secretToken , self.role) 436 | return requestGet(self.url['depth'], payload) 437 | 438 | if self.role == 'okcoin': 439 | body = requestBody(self.url['depth'], self.url['host']) 440 | if symbol: 441 | params = 'symbol=%(symbol)s' %{'symbol':symbol} 442 | else: 443 | params = '' 444 | 445 | params += '&size=%(size)s&merge=%(merge)s' %{'size':size,'merge':merge} 446 | #print params 447 | r = httpGet(self.url['host'], body, params) 448 | return r 449 | 450 | if self.role == 'huobi': 451 | # init huobi depth list to the same format as okcoin 452 | r = {} 453 | return r 454 | 455 | def fast_ticker(self): 456 | if self.role == 'default' or self.role == 'haobtc': 457 | return requestGet(self.url['fast_ticker']) 458 | 459 | -------------------------------------------------------------------------------- /arbitrage/lib/helpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | #用于进行http请求,以及MD5加密,生成签名的工具类 4 | # import httplib 5 | import http.client as httplib 6 | import urllib.parse 7 | import urllib 8 | import json 9 | import hashlib 10 | import time 11 | import math 12 | import decimal 13 | import hmac 14 | import base64 15 | import requests 16 | import traceback 17 | import logging 18 | 19 | def md5(str): 20 | m = hashlib.md5() 21 | m.update(str) 22 | return m.hexdigest() 23 | 24 | def requestGet(url,payload=''): 25 | try : 26 | r = requests.get(url,payload) 27 | if r and r.status_code == 200: 28 | return json.loads(r.text) 29 | else: 30 | return handle_error('API','API Error') 31 | except Exception as ex: 32 | logging.warn("exception requestGet:%s" % ex) 33 | traceback.print_exc() 34 | return False 35 | 36 | 37 | def requestPost(url, payload): 38 | try : 39 | r = requests.post(url,payload) 40 | if r and r.status_code == 200: 41 | return json.loads(r.text) 42 | else: 43 | return handle_error('API', r.text) 44 | except Exception as ex: 45 | logging.warn("exception requestPost:%s" % ex) 46 | traceback.print_exc() 47 | return False 48 | 49 | def buildSign(params, secretKey, host='haobtc'): 50 | if host =='haobtc' or host == 'default': 51 | sign = '' 52 | for key in sorted(params.keys()): 53 | sign += key + '=' + str(params[key]) +'&' 54 | data = sign+'secret_key='+secretKey 55 | return hashlib.md5(data.encode("utf8")).hexdigest().upper() 56 | 57 | if host == 'okcoin': 58 | sign = '' 59 | for key in sorted(params.keys()): 60 | sign += key + '=' + str(params[key]) +'&' 61 | data = sign+'secret_key='+secretKey 62 | return hashlib.md5(data.encode("utf8")).hexdigest().upper() 63 | 64 | if host == '': 65 | return 66 | 67 | if host == '': 68 | return 69 | 70 | def httpGet(url, resource, params=''): 71 | try : 72 | conn = httplib.HTTPSConnection(url, timeout=10) 73 | conn.request("GET",resource + '?' + params) 74 | response = conn.getresponse() 75 | data = response.read().decode('utf-8') 76 | return json.loads(data) 77 | except: 78 | return False 79 | 80 | def httpPost(url,resource,params): 81 | headers = { 82 | "Content-type" : "application/x-www-form-urlencoded", 83 | } 84 | try : 85 | conn = httplib.HTTPSConnection(url, timeout=10) 86 | temp_params = urllib.parse.urlencode(params) 87 | conn.request("POST", resource, temp_params, headers) 88 | response = conn.getresponse() 89 | data = response.read().decode('utf-8') 90 | params.clear() 91 | conn.close() 92 | return data 93 | except: 94 | # except Exception,e: 95 | # print(Exception,":",e) 96 | traceback.print_exc() 97 | return False 98 | 99 | 100 | def signature(params): 101 | params = sorted(params.items(), key=lambda d:d[0], reverse=False) 102 | message = urllib.parse.urlencode(params) 103 | message = message.encode('utf-8') 104 | 105 | m = hashlib.md5() 106 | m.update(message) 107 | m.digest() 108 | sig=m.hexdigest() 109 | return sig 110 | 111 | 112 | def requestBody(url,host): 113 | arr = url.split(host) 114 | return arr[1] 115 | 116 | def tradeLoad(params, secretKey, host='haobtc'): 117 | params['sign'] = buildSign(params, secretKey, host) 118 | return params 119 | 120 | def fen2yuan(amount , default=100): 121 | return (Decimal(amount)/Decimal(default)).quantize(decimal.Decimal('1E-2')) 122 | 123 | def satoshi2btc(amount, default ='1E-4'): 124 | return (Decimal(amount)/Decimal(COIN)).quantize(decimal.Decimal(default)) 125 | 126 | def decimal_default(obj): 127 | if isinstance(obj, Decimal): 128 | return float(obj) 129 | else: 130 | return str(obj) 131 | 132 | def batchTradeFormat(arr): 133 | return str(arr).replace(' ','').replace("'",'"') 134 | 135 | def str2int(num): 136 | return int(float(num)) 137 | 138 | def local_time(time_utc): 139 | u = int(time.mktime(time_utc.timetuple())) 140 | time_local = datetime.datetime.fromtimestamp(u, pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S') 141 | return str(time_local) 142 | 143 | def handle_error(code, message, status=404): 144 | resp = {'code': code, 'message': message} 145 | return resp 146 | -------------------------------------------------------------------------------- /arbitrage/lib/push.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import time 4 | import os 5 | import math 6 | import os, time 7 | import sys 8 | import traceback 9 | 10 | class Push: 11 | def __init__(self, zmq_port, zmq_host=None): 12 | self.zmq_port = zmq_port 13 | self.zmq_host = zmq_host 14 | self.is_terminated = False 15 | 16 | def terminate(self): 17 | self.is_terminated = True 18 | 19 | def process_message(self,message): 20 | pass 21 | 22 | def msg_server(self): 23 | import zmq 24 | import time 25 | context = zmq.Context() 26 | socket = context.socket(zmq.PULL) 27 | socket.bind("tcp://*:%s"%self.zmq_port) 28 | 29 | logging.info("zmq msg_server start...") 30 | while not self.is_terminated: 31 | # Wait for next request from client 32 | message = socket.recv() 33 | logging.info("new pull message: %s", message) 34 | self.process_message(message) 35 | 36 | time.sleep (1) # Do some 'work' 37 | 38 | def notify_obj(self, pyObj): 39 | import zmq 40 | try: 41 | context = zmq.Context() 42 | socket = context.socket(zmq.PUSH) 43 | 44 | socket.connect ("tcp://%s:%s" % (self.zmq_host, self.zmq_port)) 45 | 46 | message = json.dumps(pyObj) 47 | logging.info( "notify message %s", message) 48 | 49 | socket.send_string(message) 50 | except Exception as e: 51 | logging.warn("notify_msg Exception", exc_info=True) 52 | pass 53 | 54 | def notify_msg(self, type, price): 55 | message = {'type':type, 'price':price} 56 | self.notify_obj(message) -------------------------------------------------------------------------------- /arbitrage/lib/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | sys.path.insert(0,parentdir) 5 | 6 | HAOBTC_API_URL = { 7 | 'host' :'api.bixin.com', 8 | 'trade':'https://api.bixin.com/exchange/api_hermes/v1/trade', 9 | 'cancel_order':'https://api.bixin.com/exchange/api_hermes/v1/cancel_order', 10 | 'order_info':'https://api.bixin.com/exchange/api_hermes/v1/order_info', 11 | 'orders_info':'https://api.bixin.com/exchange/api_hermes/v1/orders_info', 12 | 'history_info':'https://api.bixin.com/exchange/api_hermes/v1/history_info', 13 | 'account_info':'https://api.bixin.com/exchange/api_hermes/v1/account_info', 14 | 'ticker':'https://api.bixin.com/exchange/api_hermes/v1/ticker' , 15 | 'depth':'https://api.bixin.com/exchange/api_hermes/v1/depth', 16 | 'batch_trade':'https://api.bixin.com/exchange/api_hermes/v1/batch_trade', 17 | 'cancel_all':'https://api.bixin.com/exchange/api_hermes/v1/cancel_all', 18 | 'cancel_list':'https://api.bixin.com/exchange/api_hermes/v1/cancel_list', 19 | 'fast_ticker':'https://api.bixin.com/api/v1/price/cny', 20 | } 21 | HAOBTC_API = {'fee':0.001} 22 | 23 | HUOBI_API_URL = { 24 | 'host':'api.huobi.com/apiv3', 25 | 'ticker':'http://api.huobi.com/staticmarket/ticker_btc_json.js', 26 | 'depth':'http://api.huobi.com/staticmarket/depth_btc_json.js', 27 | 'data':'http://api.huobi.com/staticmarket/detail_btc_json.js', 28 | 'buy' : 'buy', 29 | 'buy_market': 'buy_market', 30 | 'cancel_order': 'cancel_order', 31 | 'account_info': 'get_account_info', 32 | 'new_deal_orders': 'get_new_deal_orders', 33 | 'order_id_by_trade_id': 'get_order_id_by_trade_id', 34 | 'get_orders': 'get_orders', 35 | 'order_info': 'order_info', 36 | 'sell': 'sell', 37 | 'sell_market': 'sell_market', 38 | } 39 | 40 | 41 | OKCOIN_API_URL = { 42 | 'host':'www.okcoin.cn', 43 | 'ticker': 'https://www.okcoin.cn/api/v1/ticker.do', 44 | 'depth': 'https://www.okcoin.cn/api/v1/depth.do', 45 | 'tradesInfo':'https://www.okcoin.cn/api/v1/trades.do', 46 | 'userInfo':'https://www.okcoin.cn/api/v1/userinfo.do', 47 | 'trade':'https://www.okcoin.cn/api/v1/trade.do', 48 | 'batch_trade':'https://www.okcoin.cn/api/v1/batch_trade.do', 49 | 'cancel_order':'https://www.okcoin.cn/api/v1/cancel_order.do', 50 | 'order_info':'https://www.okcoin.cn/api/v1/order_info.do', 51 | 'order_history':'https://www.okcoin.cn/api/v1/order_history.do' 52 | } 53 | 54 | OKCOIN_MIN_TRADE = {'buy':0.01,'sell':0.01 , 'trade':0.01} 55 | OKCOIN_API = {'max_open_order':50,'fee':0.004} 56 | 57 | 58 | #IMPORT local_settings 59 | try: 60 | from .local_settings import * 61 | except ImportError: 62 | pass 63 | 64 | -------------------------------------------------------------------------------- /arbitrage/observers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artooze/crypto-arbitrager/49aaac9255953cb6c36ab16b09d024afd0dd0935/arbitrage/observers/__init__.py -------------------------------------------------------------------------------- /arbitrage/observers/balancedumper.py: -------------------------------------------------------------------------------- 1 | from .observer import Observer 2 | import json 3 | import time 4 | import os 5 | from private_markets import haobtccny,huobicny,okcoincny,brokercny 6 | import sys 7 | import traceback 8 | import config 9 | import logging 10 | from .emailer import send_email 11 | 12 | class BalanceDumper(Observer): 13 | exchange = 'BrokerCNY' 14 | 15 | out_dir = 'balance_history/' 16 | 17 | def __init__(self): 18 | self.clients = { 19 | # "HaobtcCNY": haobtccny.PrivateHaobtcCNY(config.HAOBTC_API_KEY, config.HAOBTC_SECRET_TOKEN), 20 | "BrokerCNY": brokercny.PrivateBrokerCNY(), 21 | } 22 | 23 | self.cny_balance = 0 24 | self.btc_balance = 0 25 | self.cny_frozen = 0 26 | self.btc_frozen = 0 27 | self.cny_total = 0 28 | self.btc_total = 0 29 | 30 | try: 31 | os.mkdir(self.out_dir) 32 | except: 33 | pass 34 | 35 | def update_trade_history(self, exchange, time, price, cny, btc, cny_b, btc_b, cny_f, btc_f): 36 | filename = self.out_dir + exchange + '_balance.csv' 37 | need_header = False 38 | 39 | if not os.path.exists(filename): 40 | need_header = True 41 | 42 | fp = open(filename, 'a+') 43 | 44 | if need_header: 45 | fp.write("timestamp, price, cny, btc, cny_b, btc_b, cny_f, btc_f\n") 46 | 47 | fp.write(("%d") % time +','+("%.f") % price+','+("%.f") % cny+','+ str(("%.2f") % btc) +','+ str(("%.f") % cny_b)+','+ str(("%.2f") % btc_b)+','+ str(("%.f") % cny_f)+','+ str(("%.2f") % btc_f)+'\n') 48 | fp.close() 49 | 50 | def update_balance(self): 51 | for kclient in self.clients: 52 | self.clients[kclient].get_info() 53 | self.cny_balance = self.clients[kclient].cny_balance 54 | self.btc_balance = self.clients[kclient].btc_balance 55 | 56 | self.cny_frozen = self.clients[kclient].cny_frozen 57 | self.btc_frozen = self.clients[kclient].btc_frozen 58 | 59 | def cny_balance_total(self, price): 60 | return self.cny_balance + self.cny_frozen+ (self.btc_balance + self.btc_frozen)* price 61 | 62 | def btc_balance_total(self, price): 63 | return self.btc_balance + self.btc_frozen + (self.cny_balance +self.cny_frozen ) / (price*1.0) 64 | 65 | 66 | def begin_opportunity_finder(self, depths): 67 | # Update client balance 68 | self.update_balance() 69 | 70 | # get price 71 | try: 72 | bid_price = int(depths[self.exchange]["bids"][0]['price']) 73 | ask_price = int(depths[self.exchange]["asks"][0]['price']) 74 | except Exception as ex: 75 | logging.warn("exception depths:%s" % ex) 76 | t,v,tb = sys.exc_info() 77 | print(t,v) 78 | traceback.print_exc() 79 | 80 | # logging.warn(depths) 81 | return 82 | 83 | if bid_price == 0 or ask_price == 0: 84 | logging.warn("exception ticker") 85 | return 86 | 87 | cny_abs = abs(self.cny_total - self.cny_balance_total(bid_price)) 88 | cny_diff = self.cny_total*0.1 89 | btc_abs = abs(self.btc_total - self.btc_balance_total(ask_price)) 90 | btc_diff = self.btc_total*0.1 91 | 92 | self.cny_total = self.cny_balance_total(bid_price) 93 | self.btc_total = self.btc_balance_total(ask_price) 94 | 95 | if (cny_abs > 200 and cny_abs < cny_diff) or (btc_abs > 0.1 and btc_abs < btc_diff): 96 | logging.info("update_balance-->") 97 | self.update_trade_history(self.exchange, time.time(), bid_price, 98 | self.cny_total, self.btc_total, 99 | self.cny_balance, self.btc_balance, 100 | self.cny_frozen, self.btc_frozen) 101 | 102 | def end_opportunity_finder(self): 103 | pass 104 | 105 | def opportunity(self, profit, volume, buyprice, kask, sellprice, kbid, perc, weighted_buyprice, weighted_sellprice): 106 | pass 107 | -------------------------------------------------------------------------------- /arbitrage/observers/basicbot.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .observer import Observer 3 | import json 4 | import time 5 | import os 6 | import math 7 | import os, time 8 | import sys 9 | import traceback 10 | import config 11 | 12 | class BasicBot(Observer): 13 | def __init__(self): 14 | super().__init__() 15 | 16 | self.orders = [] 17 | 18 | self.max_maker_volume = config.MAKER_MAX_VOLUME 19 | self.min_maker_volume = config.MAKER_MIN_VOLUME 20 | self.max_taker_volume = config.TAKER_MAX_VOLUME 21 | self.min_taker_volume = config.TAKER_MIN_VOLUME 22 | 23 | logging.info('BasicBot Setup complete') 24 | 25 | def process_message(self,message): 26 | pass 27 | 28 | def msg_server(self): 29 | import zmq 30 | import time 31 | context = zmq.Context() 32 | socket = context.socket(zmq.PULL) 33 | socket.bind("tcp://*:%s"%config.ZMQ_PORT) 34 | 35 | logging.info("zmq msg_server start...") 36 | while not self.is_terminated: 37 | # Wait for next request from client 38 | message = socket.recv() 39 | logging.info("new pull message: %s", message) 40 | self.process_message(message) 41 | 42 | time.sleep (1) # Do some 'work' 43 | 44 | def notify_obj(self, pyObj): 45 | import zmq 46 | try: 47 | context = zmq.Context() 48 | socket = context.socket(zmq.PUSH) 49 | 50 | socket.connect ("tcp://%s:%s" % (config.ZMQ_HOST, config.ZMQ_PORT)) 51 | time.sleep(1) 52 | 53 | logging.info( "notify message %s", json.dumps(pyObj)) 54 | 55 | socket.send_string(json.dumps(pyObj)) 56 | except Exception as e: 57 | logging.warn("notify_msg Exception") 58 | pass 59 | 60 | def notify_msg(self, type, price): 61 | message = {'type':type, 'price':price} 62 | self.notify_obj(message) 63 | 64 | def new_order(self, kexchange, type, maker_only=True, amount=None, price=None): 65 | if type == 'buy' or type == 'sell': 66 | if not price or not amount: 67 | if type == 'buy': 68 | price = self.get_buy_price() 69 | amount = math.floor((self.cny_balance/price)*10)/10 70 | else: 71 | price = self.get_sell_price() 72 | amount = math.floor(self.btc_balance * 10) / 10 73 | 74 | if maker_only: 75 | amount = min(self.max_maker_volume, amount) 76 | if amount < self.min_maker_volume: 77 | logging.warn('Maker amount is too low %s %s' % (type, amount)) 78 | return None 79 | else: 80 | amount = min(self.max_taker_volume, amount) 81 | if amount < self.min_taker_volume: 82 | logging.warn('Taker amount is too low %s %s' % (type, amount)) 83 | return None 84 | 85 | if maker_only: 86 | if type == 'buy': 87 | order_id = self.clients[kexchange].buy_maker(amount, price) 88 | else: 89 | order_id = self.clients[kexchange].sell_maker(amount, price) 90 | else: 91 | if type == 'buy': 92 | order_id = self.clients[kexchange].buy(amount, price) 93 | else: 94 | order_id = self.clients[kexchange].sell(amount, price) 95 | 96 | if not order_id: 97 | logging.warn("%s @%s %f/%f BTC failed, %s" % (type, kexchange, amount, price, order_id)) 98 | return None 99 | 100 | if order_id == -1: 101 | logging.warn("%s @%s %f/%f BTC failed, %s" % (type, kexchange, amount, price, order_id)) 102 | return None 103 | 104 | order = { 105 | 'market': kexchange, 106 | 'id': order_id, 107 | 'price': price, 108 | 'amount': amount, 109 | 'deal_amount':0, 110 | 'deal_index': 0, 111 | 'type': type, 112 | 'maker_only': maker_only, 113 | 'time': time.time() 114 | } 115 | self.orders.append(order) 116 | logging.verbose("submit order %s" % (order)) 117 | 118 | return order 119 | 120 | return None 121 | 122 | 123 | def cancel_order(self, kexchange, type, order_id): 124 | result = self.clients[kexchange].cancel_order(order_id) 125 | if not result: 126 | logging.warn("cancel %s #%s failed" % (type, order_id)) 127 | return False 128 | else: 129 | return True 130 | 131 | def remove_order(self, order_id): 132 | self.orders = [x for x in self.orders if not x['id'] == order_id] 133 | 134 | def get_orders(self, type): 135 | orders_snapshot = [x for x in self.orders if x['type'] == type] 136 | return orders_snapshot 137 | 138 | def selling_len(self): 139 | return len(self.get_orders('sell')) 140 | 141 | def buying_len(self): 142 | return len(self.get_orders('buy')) 143 | 144 | def is_selling(self): 145 | return len(self.get_orders('sell')) > 0 146 | 147 | def is_buying(self): 148 | return len(self.get_orders('buy')) > 0 149 | 150 | def get_sell_price(self): 151 | return self.sellprice 152 | 153 | def get_buy_price(self): 154 | return self.buyprice 155 | 156 | def get_spread(self): 157 | return self.sellprice - self.buyprice 158 | -------------------------------------------------------------------------------- /arbitrage/observers/bitstar_mm.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .observer import Observer 3 | import json 4 | import time 5 | import os 6 | from private_markets import bitstampusd,bitstarcny,huobicny,okcoincny 7 | import math 8 | import os, time 9 | import sys 10 | import traceback 11 | import config 12 | from .basicbot import BasicBot 13 | import threading 14 | 15 | class MarketMaker(BasicBot): 16 | exchange = 'BitstarCNY' 17 | out_dir = 'trade_history/' 18 | try: 19 | filename = exchange + config.ENV+ '.csv' 20 | except Exception as e: 21 | filename = exchange + '.csv' 22 | 23 | 24 | def __init__(self): 25 | super().__init__() 26 | 27 | self.clients = { 28 | # TODO: move that to the config file 29 | "BitstarCNY": bitstarcny.PrivateBitstarCNY(config.BITSTAR_API_KEY, config.BITSTAR_SECRET_TOKEN), 30 | } 31 | 32 | self.trade_timeout = 10 # in seconds 33 | 34 | self.cny_balance = 0 35 | self.btc_balance = 0 36 | self.cny_total = 0 37 | self.btc_total = 0 38 | 39 | self.bid_fee_rate = config.bid_fee_rate 40 | self.ask_fee_rate = config.ask_fee_rate 41 | self.bid_price_risk = config.bid_price_risk 42 | self.ask_price_risk = config.ask_price_risk 43 | 44 | self.peer_exchange ='StandardCNY' 45 | # self.peer_exchange ='HuobiCNY' 46 | 47 | try: 48 | os.mkdir(self.out_dir) 49 | except: 50 | pass 51 | 52 | self.clients[self.exchange].cancel_all() 53 | 54 | logging.info('MarketMaker Setup complete') 55 | # time.sleep(2) 56 | 57 | def terminate(self): 58 | super().terminate() 59 | 60 | self.clients[self.exchange].cancel_all() 61 | 62 | logging.info('terminate complete') 63 | 64 | def hedge_order(self, order, result): 65 | pass 66 | 67 | def market_maker(self, depths): 68 | kexchange = self.exchange 69 | 70 | # update price 71 | try: 72 | bid_price = int(depths[self.exchange]["bids"][0]['price']) 73 | ask_price = int(depths[self.exchange]["asks"][0]['price']) 74 | bid_amount = (depths[self.exchange]["bids"][0]['amount']) 75 | ask_amount= (depths[self.exchange]["asks"][0]['amount']) 76 | 77 | bid1_price = int(depths[self.exchange]["bids"][1]['price']) 78 | ask1_price = int(depths[self.exchange]["asks"][1]['price']) 79 | peer_bid_price = int(depths[self.peer_exchange]["bids"][0]['price']) 80 | peer_ask_price = int(depths[self.peer_exchange]["asks"][0]['price']) 81 | 82 | except Exception as ex: 83 | logging.warn("exception depths:%s" % ex) 84 | traceback.print_exc() 85 | return 86 | 87 | if bid_price == 0 or ask_price == 0 or peer_bid_price == 0 or peer_bid_price == 0: 88 | logging.warn("exception ticker") 89 | return 90 | 91 | if bid_price+1 < ask_price : 92 | buyprice = bid_price + 1 93 | else: 94 | buyprice = bid_price 95 | 96 | if ask_price-1 > bid_price: 97 | sellprice = ask_price - 1 98 | else: 99 | sellprice = ask_price 100 | 101 | if buyprice == sellprice: 102 | if buyprice > bid_price: 103 | buyprice -=1 104 | elif sellprice < ask_price: 105 | sellprice +=1 106 | 107 | peer_bid_hedge_price = int(peer_bid_price*(1+self.bid_fee_rate)) 108 | peer_ask_hedge_price = int(peer_ask_price*(1-self.ask_fee_rate)) 109 | 110 | buyprice=min(buyprice, peer_bid_hedge_price) - self.bid_price_risk 111 | sellprice=max(sellprice, peer_ask_hedge_price) + self.ask_price_risk 112 | logging.debug("sellprice/buyprice=(%s/%s)" % (sellprice, buyprice)) 113 | 114 | self.buyprice = buyprice 115 | self.sellprice = sellprice 116 | 117 | # Update client balance 118 | self.update_balance() 119 | 120 | # query orders 121 | if self.is_buying(): 122 | for buy_order in self.get_orders('buy'): 123 | logging.debug(buy_order) 124 | result = self.clients[kexchange].get_order(buy_order['id']) 125 | logging.debug(result) 126 | if not result: 127 | logging.warn("get_order buy #%s failed" % (buy_order['id'])) 128 | return 129 | 130 | self.hedge_order(buy_order, result) 131 | 132 | if result['status'] == 'CLOSE' or result['status'] == 'CANCELED': 133 | self.remove_order(buy_order['id']) 134 | else: 135 | current_time = time.time() 136 | if (result['price'] != buyprice) and \ 137 | ((result['price'] > peer_bid_hedge_price) or \ 138 | ( current_time - buy_order['time'] > self.trade_timeout and \ 139 | (result['price'] < bid_price or result['price'] > (bid1_price + 1)))): 140 | logging.info("[TraderBot] cancel last buy trade " + 141 | "occured %.2f seconds ago" % 142 | (current_time - buy_order['time'])) 143 | logging.info("cancel buyprice %s result['price'] = %s[%s]" % (buyprice, result['price'], result['price'] != buyprice)) 144 | 145 | self.cancel_order(kexchange, 'buy', buy_order['id']) 146 | 147 | 148 | if self.is_selling(): 149 | for sell_order in self.get_orders('sell'): 150 | logging.debug(sell_order) 151 | result = self.clients[kexchange].get_order(sell_order['id']) 152 | logging.debug(result) 153 | if not result: 154 | logging.warn("get_order sell #%s failed" % (sell_order['id'])) 155 | return 156 | 157 | self.hedge_order(sell_order, result) 158 | 159 | if result['status'] == 'CLOSE' or result['status'] == 'CANCELED': 160 | self.remove_order(sell_order['id']) 161 | else: 162 | current_time = time.time() 163 | if (result['price'] != sellprice) and \ 164 | ((result['price'] < peer_ask_hedge_price) or \ 165 | (current_time - sell_order['time'] > self.trade_timeout and \ 166 | (result['price'] > ask_price or result['price'] < (ask1_price - 1)))): 167 | logging.info("[TraderBot] cancel last SELL trade " + 168 | "occured %.2f seconds ago" % 169 | (current_time - sell_order['time'])) 170 | logging.info("cancel sellprice %s result['price'] = %s [%s]" % (sellprice, result['price'], result['price'] != sellprice)) 171 | 172 | self.cancel_order(kexchange, 'sell', sell_order['id']) 173 | 174 | # excute trade 175 | if self.buying_len() < config.MAKER_BUY_QUEUE: 176 | self.new_order_notify(kexchange, 'buy') 177 | if self.selling_len() < config.MAKER_SELL_QUEUE: 178 | self.new_order_notify(kexchange, 'sell') 179 | 180 | def update_trade_history(self, time, price, cny, btc): 181 | filename = self.out_dir + self.filename 182 | need_header = False 183 | 184 | if not os.path.exists(filename): 185 | need_header = True 186 | 187 | fp = open(filename, 'a+') 188 | 189 | if need_header: 190 | fp.write("timestamp, price, cny, btc\n") 191 | 192 | fp.write(("%d") % time +','+("%.2f") % price+','+("%.2f") % cny+','+ str(("%.4f") % btc) +'\n') 193 | fp.close() 194 | 195 | def update_balance(self): 196 | for kclient in self.clients: 197 | if kclient == self.exchange: 198 | self.clients[kclient].get_info() 199 | self.cny_balance = self.clients[kclient].cny_balance 200 | self.btc_balance = self.clients[kclient].btc_balance 201 | 202 | self.cny_frozen = self.clients[kclient].cny_frozen 203 | self.btc_frozen = self.clients[kclient].btc_frozen 204 | 205 | cny_abs = abs(self.cny_total - self.cny_balance_total(self.buyprice)) 206 | cny_diff = self.cny_total*0.1 207 | btc_abs = abs(self.btc_total - self.btc_balance_total(self.sellprice)) 208 | btc_diff = self.btc_total*0.1 209 | 210 | self.cny_total = self.cny_balance_total(self.buyprice) 211 | self.btc_total = self.btc_balance_total(self.sellprice) 212 | 213 | if (cny_abs > 5 and cny_abs < cny_diff) or (btc_abs > 0.001 and btc_abs < btc_diff): 214 | logging.debug("update_balance-->") 215 | self.update_trade_history(time.time(), self.buyprice, self.cny_total, self.btc_total) 216 | 217 | logging.debug("cny_balance=%s/%s, btc_balance=%s/%s, total_cny=%0.2f, total_btc=%0.2f", 218 | self.cny_balance, self.cny_frozen, self.btc_balance, self.btc_frozen, 219 | self.cny_balance_total(self.buyprice), self.btc_balance_total(self.sellprice)) 220 | 221 | def cny_balance_total(self, price): 222 | return self.cny_balance + self.cny_frozen+ (self.btc_balance + self.btc_frozen)* price 223 | 224 | def btc_balance_total(self, price): 225 | return self.btc_balance + self.btc_frozen + (self.cny_balance +self.cny_frozen ) / (price*1.0) 226 | 227 | def new_order_notify(self, kexchange, type, maker_only=True, amount=None, price=None): 228 | order = super().new_order(kexchange, type, maker_only, amount, price) 229 | 230 | if order: 231 | # self.notify_msg(order['type'], order['price']) 232 | t = threading.Thread(target = self.notify_msg, args=(order['type'], order['price'],)) 233 | t.start() 234 | logging.info("current has %d threads" % (threading.activeCount() - 1)) 235 | 236 | def begin_opportunity_finder(self, depths): 237 | self.market_maker(depths) 238 | 239 | def end_opportunity_finder(self): 240 | pass 241 | 242 | def opportunity(self, profit, volume, buyprice, kask, sellprice, kbid, perc, weighted_buyprice, weighted_sellprice): 243 | pass 244 | -------------------------------------------------------------------------------- /arbitrage/observers/btccpro_okspot.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import config 3 | import time 4 | from .observer import Observer 5 | from .emailer import send_email 6 | from fiatconverter import FiatConverter 7 | from private_markets import okcoincny,btccprocny 8 | import os, time 9 | import sys 10 | import traceback 11 | from .basicbot import BasicBot 12 | 13 | # python3 arbitrage/arbitrage.py -oBTCCPro_OkSpot -mBtccProCNY,OKCoinCNY 14 | 15 | class BTCCPro_OkSpot(BasicBot): 16 | exchange = 'BtccProCNY' 17 | hedger = 'OKCoinCNY' 18 | 19 | def __init__(self): 20 | super().__init__() 21 | 22 | self.clients = { 23 | "OKCoinCNY": okcoincny.PrivateOkCoinCNY(config.OKCOIN_API_KEY, config.OKCOIN_SECRET_TOKEN), 24 | "BtccProCNY": btccprocny.PrivateBtccProCNY(), 25 | } 26 | 27 | self.trade_wait = config.trade_wait # in seconds 28 | self.last_trade = 0 29 | 30 | self.init_btc = {'OKCoinCNY':500, 'BtccProCNY':500} 31 | self.init_cny = {'OKCoinCNY':100, 'BtccProCNY':100} 32 | 33 | self.spread = 0.1 34 | 35 | self.simluate = True 36 | 37 | t = threading.Thread(target = self.msg_server) 38 | t.start() 39 | logging.info('BTCCPro_OkSpot Setup complete') 40 | # time.sleep(2) 41 | 42 | def process_message(self,message): 43 | kexchange = self.exchange 44 | 45 | try: 46 | message = message.decode('utf-8') 47 | message = json.loads(message) 48 | logging.info('msg:%s', message) 49 | type = message['type'] 50 | price = message['price'] 51 | 52 | logging.info('msg type:%s %s', type, price) 53 | 54 | if type == 'buy': 55 | buy_orders = self.get_orders('buy') 56 | buy_orders.sort(key=lambda x: x['price'], reverse=True) 57 | 58 | for buy_order in buy_orders: 59 | if buy_order['price'] == price: 60 | self.cancel_order(kexchange, 'buy', buy_order['id']) 61 | break 62 | elif type == 'sell': 63 | sell_orders = self.get_orders('sell') 64 | sell_orders.sort(key=lambda x: x['price']) 65 | 66 | for sell_order in sell_orders: 67 | if sell_order['price'] == price: 68 | self.cancel_order(kexchange, 'sell', sell_order['id']) 69 | break 70 | except Exception as e: 71 | logging.error("process message exception %s", e) 72 | traceback.print_exc() 73 | 74 | 75 | def hedgeALG1(self, depths): 76 | # update price 77 | try: 78 | bid_price = (depths[self.exchange]["bids"][0]['price']) 79 | ask_price = (depths[self.exchange]["asks"][0]['price']) 80 | bid_amount = int(depths[self.exchange]["bids"][0]['amount']) 81 | ask_amount= int(depths[self.exchange]["asks"][0]['amount']) 82 | 83 | hedger_bid_price = (depths[self.hedger]["bids"][0]['price']) 84 | hedger_ask_price = (depths[self.hedger]["asks"][0]['price']) 85 | hedger_bid_amount = (depths[self.hedger]["bids"][0]['amount']) 86 | hedger_ask_amount = (depths[self.hedger]["asks"][0]['amount']) 87 | 88 | except Exception as ex: 89 | logging.warn("exception depths:%s" % ex) 90 | traceback.print_exc() 91 | return 92 | 93 | if bid_price == 0 or ask_price == 0 or hedger_bid_price == 0 or hedger_ask_price == 0: 94 | logging.info("exception ticker %s %s %s %s", bid_price ,ask_price,hedger_bid_price,hedger_ask_price) 95 | return 96 | 97 | # 并行程序一 98 | # 当BTCC价格(BTCC买一)减去OKCOIN的价格(OKCOIN卖一)大于-2时,买X个OKCOIN现货(价格为卖一价+0.1元方便成交), 99 | # 卖出X个BTCC现货(X数量为BTCC买一的数量) 100 | 101 | 102 | # 并行程序二 103 | # 当BTCC价格减去OKCOIN的价格小于-8时,卖Y个OKCOIN现货(买一价-0.1元),买入Y个BTCC现货 (Y数量为BTCC卖一的数量) 104 | 105 | 106 | # -2 -8 0.1 这三个值将来为变量 107 | 108 | 109 | # Update client balance 110 | self.update_balance() 111 | 112 | logging.info("maker:%s %s %s %s", bid_price, bid_amount, ask_price, ask_amount) 113 | logging.info("hedger:%s %s %s %s", hedger_bid_price, hedger_bid_amount, hedger_ask_price, hedger_ask_amount) 114 | 115 | logging.info("bid_price - hedger_ask_price=%0.2f", bid_price - hedger_ask_price) 116 | logging.info("ask_price - hedger_bid_price=%0.2f", ask_price - hedger_bid_price) 117 | 118 | current_time = time.time() 119 | if current_time - self.last_trade < self.trade_wait: 120 | logging.warn("Can't automate this trade, last trade " + 121 | "occured %.2f seconds ago" % 122 | (current_time - self.last_trade)) 123 | return 124 | 125 | if bid_price - hedger_ask_price > -2: 126 | 127 | hedge_amount = int(min(bid_amount, 1)) 128 | if hedge_amount < 1: 129 | logging.warn("sell in btcc %s , buy in ok %s ..too small [%s]btc", bid_price, hedger_ask_price, hedge_amount) 130 | return 131 | 132 | btc_balance = int(self.clients[self.exchange].cny_balance/(bid_price-self.spread)) 133 | if btc_balance < hedge_amount: 134 | logging.warn("btcc btc balance %s insufficent", btc_balance) 135 | return 136 | 137 | if self.clients[self.hedger].cny_balance < hedge_amount*(hedger_ask_price+self.spread): 138 | logging.warn("okcoin cny balance %s insufficent", self.clients[self.hedger].cny_balance ) 139 | return 140 | 141 | logging.info("sell in btcc %s , buy in ok %s [%s]btc", bid_price, hedger_ask_price, hedge_amount) 142 | 143 | if not self.simluate: 144 | self.new_order(self.exchange, 'sell', maker_only=False, amount=hedge_amount, price=bid_price-self.spread) 145 | self.new_order(self.hedger, 'buy', maker_only=False, amount=hedge_amount, price=hedger_ask_price+self.spread) 146 | 147 | self.last_trade = time.time() 148 | 149 | elif ask_price - hedger_bid_price < -8 : 150 | hedge_amount = int(min(ask_amount, 1)) 151 | if hedge_amount < 1: 152 | logging.warn("sell in ok %s, buy in btcc %s ..insufficent [%s]btc", ask_price, hedger_bid_price, hedge_amount) 153 | return 154 | 155 | btc_balance = int(self.clients[self.exchange].cny_balance/(ask_price+self.spread)) 156 | if btc_balance < hedge_amount: 157 | logging.warn("btcc cny balance %s insufficent", btc_balance) 158 | return 159 | 160 | if self.clients[self.hedger].btc_balance < hedge_amount: 161 | logging.warn("okcoin btc balance %s insufficent", self.clients[self.hedger].btc_balance ) 162 | return 163 | 164 | logging.info("sell in ok %s, buy in btcc %s [%s]btc", ask_price, hedger_bid_price, hedge_amount) 165 | 166 | if not self.simluate: 167 | self.new_order(self.exchange, 'buy', maker_only=False, amount=hedge_amount, price=ask_price+self.spread) 168 | self.new_order(self.hedger, 'sell', maker_only=False, amount=hedge_amount, price=hedger_ask_price-self.spread) 169 | 170 | self.last_trade = time.time() 171 | 172 | 173 | def update_trade_history(self, time, price, cny, btc): 174 | filename = self.out_dir + self.filename 175 | need_header = False 176 | 177 | if not os.path.exists(filename): 178 | need_header = True 179 | 180 | fp = open(filename, 'a+') 181 | 182 | if need_header: 183 | fp.write("timestamp, price, cny, btc\n") 184 | 185 | fp.write(("%d") % time +','+("%.2f") % price+','+("%.2f") % cny+','+ str(("%.4f") % btc) +'\n') 186 | fp.close() 187 | 188 | def update_balance(self): 189 | for kclient in self.clients: 190 | self.clients[kclient].get_info() 191 | 192 | def begin_opportunity_finder(self, depths): 193 | self.hedgeALG1(depths) 194 | 195 | def end_opportunity_finder(self): 196 | pass 197 | 198 | def opportunity(self, profit, volume, buyprice, kask, sellprice, kbid, perc, weighted_buyprice, weighted_sellprice): 199 | pass 200 | -------------------------------------------------------------------------------- /arbitrage/observers/emailer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .observer import Observer 3 | import config 4 | import smtplib 5 | import traceback 6 | 7 | def send_email(subject, msg): 8 | import smtplib 9 | 10 | message = "From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s\r\n" % (config.EMAIL_HOST_USER, ", ".join(config.EMAIL_RECEIVER), subject, msg) 11 | try: 12 | smtpserver = smtplib.SMTP(config.EMAIL_HOST) 13 | smtpserver.set_debuglevel(0) 14 | smtpserver.ehlo() 15 | smtpserver.starttls() 16 | smtpserver.ehlo 17 | smtpserver.login(config.EMAIL_HOST_USER, config.EMAIL_HOST_PASSWORD) 18 | smtpserver.sendmail(config.EMAIL_HOST_USER, config.EMAIL_RECEIVER, message) 19 | smtpserver.quit() 20 | smtpserver.close() 21 | logging.info("send mail success") 22 | except: 23 | logging.error("send mail failed") 24 | traceback.print_exc() 25 | 26 | class Emailer(Observer): 27 | def opportunity(self, profit, volume, buyprice, kask, sellprice, kbid, perc, 28 | weighted_buyprice, weighted_sellprice): 29 | if profit > config.profit_thresh and perc > config.perc_thresh: 30 | message = """profit: %f CNY with volume: %f BTC 31 | buy at %.4f (%s) sell at %.4f (%s) ~%.2f%% 32 | """ % (profit, volume, buyprice, kask, sellprice, kbid, perc) 33 | send_email("Arbitrage Bot", message) 34 | -------------------------------------------------------------------------------- /arbitrage/observers/hedgerbot.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .observer import Observer 3 | import json 4 | import time 5 | import os 6 | import math 7 | import os, time 8 | import sys 9 | import traceback 10 | import config 11 | from private_markets import haobtccny, brokercny 12 | from .marketmaker import MarketMaker 13 | import threading 14 | 15 | class HedgerBot(MarketMaker): 16 | exchange = 'HaobtcCNY' 17 | hedger = 'BrokerCNY' 18 | out_dir = 'hedger_history/' 19 | filename = exchange + '-bot.csv' 20 | 21 | def __init__(self): 22 | super().__init__() 23 | 24 | self.clients = { 25 | "HaobtcCNY": haobtccny.PrivateHaobtcCNY(config.HAOBTC_API_KEY, config.HAOBTC_SECRET_TOKEN), 26 | "BrokerCNY": brokercny.PrivateBrokerCNY(), 27 | } 28 | 29 | self.taker_fee = 0.002 30 | 31 | self.bid_fee_rate = config.bid_fee_rate 32 | self.ask_fee_rate = config.ask_fee_rate 33 | self.bid_price_risk = config.bid_price_risk 34 | self.ask_price_risk = config.ask_price_risk 35 | self.peer_exchange = self.hedger 36 | 37 | try: 38 | os.mkdir(self.out_dir) 39 | except: 40 | pass 41 | 42 | t = threading.Thread(target = self.msg_server) 43 | t.start() 44 | logging.info('HedgerBot Setup complete') 45 | # time.sleep(2) 46 | 47 | def process_message(self,message): 48 | kexchange = self.exchange 49 | 50 | try: 51 | message = message.decode('utf-8') 52 | message = json.loads(message) 53 | logging.info('msg:%s', message) 54 | type = message['type'] 55 | price = message['price'] 56 | 57 | logging.info('msg type:%s %s', type, price) 58 | 59 | if type == 'buy': 60 | buy_orders = self.get_orders('buy') 61 | buy_orders.sort(key=lambda x: x['price'], reverse=True) 62 | 63 | for buy_order in buy_orders: 64 | if buy_order['price'] == price: 65 | self.cancel_order(kexchange, 'buy', buy_order['id']) 66 | break 67 | elif type == 'sell': 68 | sell_orders = self.get_orders('sell') 69 | sell_orders.sort(key=lambda x: x['price']) 70 | 71 | for sell_order in sell_orders: 72 | if sell_order['price'] == price: 73 | self.cancel_order(kexchange, 'sell', sell_order['id']) 74 | break 75 | except Exception as e: 76 | logging.error("process message exception %s", e) 77 | traceback.print_exc() 78 | 79 | 80 | def market_maker(self, depths): 81 | # super().market_maker(depths) 82 | kexchange = self.exchange 83 | 84 | # update price 85 | try: 86 | bid_price = int(depths[self.exchange]["bids"][0]['price']) 87 | ask_price = int(depths[self.exchange]["asks"][0]['price']) 88 | bid_amount = (depths[self.exchange]["bids"][0]['amount']) 89 | ask_amount = (depths[self.exchange]["asks"][0]['amount']) 90 | except Exception as ex: 91 | logging.warn("exception haobtc depths:%s" % ex) 92 | traceback.print_exc() 93 | bid_price = 0 94 | ask_price = 0 95 | bid_amount = 0 96 | ask_amount = 0 97 | 98 | try: 99 | peer_bid_price = int(depths[self.peer_exchange]["bids"][0]['price']) 100 | peer_ask_price = int(depths[self.peer_exchange]["asks"][0]['price']) 101 | 102 | except Exception as ex: 103 | logging.warn("exception peer depths:%s" % ex) 104 | traceback.print_exc() 105 | return 106 | 107 | if peer_bid_price == 0 or peer_ask_price == 0: 108 | logging.warn("exception ticker") 109 | return 110 | 111 | if bid_price < 1: 112 | bid_price = 100 113 | if ask_price < 1: 114 | ask_price = 100000 115 | 116 | 117 | peer_bid_price = peer_bid_price*(1-self.taker_fee) - self.bid_price_risk 118 | peer_ask_price = peer_ask_price*(1+self.taker_fee) + self.ask_price_risk 119 | 120 | buyprice = int(peer_bid_price) - 1 121 | sellprice = int(peer_ask_price) + 1 122 | 123 | min_buy_price = buyprice - config.MAKER_BUY_QUEUE*config.MAKER_BUY_STAGE 124 | max_sell_price = sellprice + config.MAKER_SELL_QUEUE*config.MAKER_SELL_STAGE 125 | 126 | self.buyprice_spread = set(range(min_buy_price+1, buyprice+1)) 127 | self.sellprice_spread = set(range(sellprice, max_sell_price)) 128 | 129 | logging.debug("%s/%s", self.sellprice_spread, self.buyprice_spread) 130 | 131 | self.buyprice = buyprice 132 | self.sellprice = sellprice 133 | 134 | # Update client balance 135 | self.update_balance() 136 | 137 | # query orders 138 | if self.is_buying(): 139 | buy_orders = self.get_orders('buy') 140 | buy_orders.sort(key=lambda x: x['price'], reverse=True) 141 | buy_prices = [x['price'] for x in buy_orders] 142 | logging.debug(buy_prices) 143 | 144 | for buy_order in buy_orders: 145 | logging.debug(buy_order) 146 | result = self.clients[kexchange].get_order(buy_order['id']) 147 | logging.debug (result) 148 | if not result: 149 | logging.warn("get_order buy #%s failed" % (buy_order['id'])) 150 | return 151 | 152 | self.hedge_order(buy_order, result) 153 | 154 | if result['status'] == 'CLOSE' or result['status'] == 'CANCELED': 155 | self.remove_order(buy_order['id']) 156 | elif (result['price'] not in self.buyprice_spread): 157 | logging.info("cancel buyprice %s result['price'] = %s" % (self.buyprice_spread, result['price'])) 158 | self.cancel_order(kexchange, 'buy', buy_order['id']) 159 | 160 | 161 | if self.is_selling(): 162 | sell_orders = self.get_orders('sell') 163 | sell_orders.sort(key=lambda x: x['price']) 164 | sell_prices = [x['price'] for x in sell_orders] 165 | logging.debug(sell_prices) 166 | for sell_order in self.get_orders('sell'): 167 | logging.debug(sell_order) 168 | result = self.clients[kexchange].get_order(sell_order['id']) 169 | logging.debug (result) 170 | if not result: 171 | logging.warn("get_order sell #%s failed" % (sell_order['id'])) 172 | return 173 | 174 | self.hedge_order(sell_order, result) 175 | 176 | if result['status'] == 'CLOSE' or result['status'] == 'CANCELED': 177 | self.remove_order(sell_order['id']) 178 | elif (result['price'] not in self.sellprice_spread): 179 | logging.info("cancel sellprice %s result['price'] = %s" % (self.sellprice_spread, result['price'])) 180 | self.cancel_order(kexchange, 'sell', sell_order['id']) 181 | 182 | # excute maker trade 183 | if config.MAKER_TRADE_ENABLE: 184 | if self.buying_len() < config.MAKER_BUY_QUEUE: 185 | self.new_order(kexchange, 'buy') 186 | if self.selling_len() < config.MAKER_SELL_QUEUE: 187 | self.new_order(kexchange, 'sell') 188 | 189 | # excute taker trade 190 | if config.TAKER_TRADE_ENABLE: 191 | taker_buy_price = peer_bid_price*(1-self.taker_fee) 192 | taker_sell_price = peer_ask_price*(1+self.taker_fee) 193 | 194 | logging.debug("price [%s,%s], peer [%s,%s] taker price:[%s,%s]", ask_price, bid_price, peer_ask_price, peer_bid_price, taker_buy_price, taker_sell_price) 195 | 196 | if ask_price!=0 and ask_price < taker_buy_price: 197 | if ask_amount < 0.1: 198 | ask_amount = 0.1 199 | ask_amount+=0.01 200 | logging.info("to taker buy %s<%s", ask_price, taker_buy_price) 201 | self.new_order(kexchange, 'buy', maker_only=False, amount=ask_amount, price=ask_price) 202 | return 203 | 204 | if bid_price != 0 and bid_price > taker_sell_price: 205 | if bid_amount < 0.1: 206 | bid_amount = 0.1 207 | bid_amount +=0.01 208 | logging.info("to taker sell %s>%s", bid_price, taker_sell_price) 209 | self.new_order(kexchange, 'sell', maker_only=False, amount= bid_amount, price=bid_price) 210 | return 211 | 212 | def get_sell_price(self): 213 | sell_orders = self.get_orders('sell') 214 | sell_prices = [x['price'] for x in sell_orders] 215 | price_candidate_set = set(self.sellprice_spread) - set(sell_prices) 216 | price_candidate_list = list(price_candidate_set) 217 | # price_candidate_list.sort() 218 | 219 | for x in price_candidate_list: 220 | return x 221 | 222 | return super().get_sell_price() 223 | 224 | logging.error (sell_orders) 225 | logging.error (sell_prices) 226 | logging.error (price_candidate_set) 227 | 228 | def get_buy_price(self): 229 | buy_orders = self.get_orders('buy') 230 | buy_prices = [x['price'] for x in buy_orders] 231 | 232 | price_candidate_set = set(self.buyprice_spread) - set(buy_prices) 233 | price_candidate_list = list(price_candidate_set) 234 | # price_candidate_list.sort(reverse=True) 235 | 236 | for x in price_candidate_list: 237 | return x 238 | 239 | return super().get_buy_price() 240 | 241 | logging.error(self.buyprice_spread) 242 | logging.error (buy_orders) 243 | logging.error (buy_prices) 244 | logging.error (price_candidate_set) 245 | 246 | def hedge_order(self, order, result): 247 | if result['deal_size'] <= 0: 248 | logging.debug("[hedger]NOTHING TO BE DEALED.") 249 | return 250 | 251 | order_id = result['order_id'] 252 | deal_size = result['deal_size'] 253 | price = result['avg_price'] 254 | 255 | amount = deal_size - order['deal_amount'] 256 | if amount <= config.broker_min_amount: 257 | logging.debug("[hedger]deal nothing while.") 258 | return 259 | 260 | maker_only = order['maker_only'] 261 | client_id = str(order_id) + '-' + str(order['deal_index'])+('' if maker_only else '-taker') 262 | 263 | logging.info("hedge new deal: %s", result) 264 | hedge_side = 'SELL' if result['side'] =='BUY' else 'BUY' 265 | logging.info('hedge [%s] to broker: %s %s %s', client_id, hedge_side, amount, price) 266 | 267 | if hedge_side == 'SELL': 268 | self.clients[self.hedger].sell(amount, price, client_id) 269 | else: 270 | self.clients[self.hedger].buy(amount, price, client_id) 271 | 272 | # update the deal_amount of local order 273 | self.remove_order(order_id) 274 | order['deal_amount'] = deal_size 275 | order['deal_index'] +=1 276 | self.orders.append(order) 277 | 278 | -------------------------------------------------------------------------------- /arbitrage/observers/historydumper.py: -------------------------------------------------------------------------------- 1 | from .observer import Observer 2 | import json 3 | import time 4 | import os 5 | import logging 6 | 7 | 8 | class HistoryDumper(Observer): 9 | out_dir = 'history/' 10 | 11 | def __init__(self): 12 | try: 13 | os.mkdir(self.out_dir) 14 | except: 15 | pass 16 | 17 | def begin_opportunity_finder(self, depths): 18 | filename = self.out_dir + 'order-book-' + \ 19 | str(int(time.time())) + '.json' 20 | fp = open(filename, 'w') 21 | json.dump(depths, fp) 22 | logging.debug (depths) 23 | 24 | def end_opportunity_finder(self): 25 | pass 26 | 27 | def opportunity(self, profit, volume, buyprice, kask, sellprice, kbid, perc, weighted_buyprice, weighted_sellprice): 28 | pass 29 | -------------------------------------------------------------------------------- /arbitrage/observers/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .observer import Observer 3 | 4 | 5 | class Logger(Observer): 6 | def opportunity(self, profit, volume, buyprice, kask, sellprice, kbid, perc, 7 | weighted_buyprice, weighted_sellprice): 8 | logging.info("profit: %f CNY with volume: %f BTC - buy at %.4f (%s) sell at %.4f (%s) ~%.2f%%" \ 9 | % (profit, volume, buyprice, kask, sellprice, kbid, perc)) 10 | -------------------------------------------------------------------------------- /arbitrage/observers/marketmaker.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .observer import Observer 3 | import json 4 | import time 5 | import os 6 | from private_markets import bitstampusd,haobtccny,huobicny,okcoincny 7 | import math 8 | import os, time 9 | import sys 10 | import traceback 11 | import config 12 | from .basicbot import BasicBot 13 | import threading 14 | 15 | class MarketMaker(BasicBot): 16 | exchange = 'HaobtcCNY' 17 | out_dir = 'trade_history/' 18 | try: 19 | filename = exchange + config.ENV+ '.csv' 20 | except Exception as e: 21 | filename = exchange + '.csv' 22 | 23 | 24 | def __init__(self): 25 | super().__init__() 26 | 27 | self.clients = { 28 | # TODO: move that to the config file 29 | "HaobtcCNY": haobtccny.PrivateHaobtcCNY(config.HAOBTC_API_KEY, config.HAOBTC_SECRET_TOKEN), 30 | } 31 | 32 | self.trade_timeout = 10 # in seconds 33 | 34 | self.cny_balance = 0 35 | self.btc_balance = 0 36 | self.cny_total = 0 37 | self.btc_total = 0 38 | 39 | self.bid_fee_rate = config.bid_fee_rate 40 | self.ask_fee_rate = config.ask_fee_rate 41 | self.bid_price_risk = config.bid_price_risk 42 | self.ask_price_risk = config.ask_price_risk 43 | 44 | self.peer_exchange ='OKCoinCNY' 45 | # self.peer_exchange ='HuobiCNY' 46 | 47 | try: 48 | os.mkdir(self.out_dir) 49 | except: 50 | pass 51 | 52 | self.clients[self.exchange].cancel_all() 53 | 54 | logging.info('MarketMaker Setup complete') 55 | # time.sleep(2) 56 | 57 | def terminate(self): 58 | super().terminate() 59 | 60 | self.clients[self.exchange].cancel_all() 61 | 62 | logging.info('terminate complete') 63 | 64 | def hedge_order(self, order, result): 65 | pass 66 | 67 | def market_maker(self, depths): 68 | kexchange = self.exchange 69 | 70 | # update price 71 | try: 72 | bid_price = int(depths[self.exchange]["bids"][0]['price']) 73 | ask_price = int(depths[self.exchange]["asks"][0]['price']) 74 | bid_amount = (depths[self.exchange]["bids"][0]['amount']) 75 | ask_amount= (depths[self.exchange]["asks"][0]['amount']) 76 | 77 | bid1_price = int(depths[self.exchange]["bids"][1]['price']) 78 | ask1_price = int(depths[self.exchange]["asks"][1]['price']) 79 | peer_bid_price = int(depths[self.peer_exchange]["bids"][0]['price']) 80 | peer_ask_price = int(depths[self.peer_exchange]["asks"][0]['price']) 81 | 82 | except Exception as ex: 83 | logging.warn("exception depths:%s" % ex) 84 | traceback.print_exc() 85 | return 86 | 87 | if bid_price == 0 or ask_price == 0 or peer_bid_price == 0 or peer_bid_price == 0: 88 | logging.warn("exception ticker") 89 | return 90 | 91 | if bid_price+1 < ask_price : 92 | buyprice = bid_price + 1 93 | else: 94 | buyprice = bid_price 95 | 96 | if ask_price-1 > bid_price: 97 | sellprice = ask_price - 1 98 | else: 99 | sellprice = ask_price 100 | 101 | if buyprice == sellprice: 102 | if buyprice > bid_price: 103 | buyprice -=1 104 | elif sellprice < ask_price: 105 | sellprice +=1 106 | 107 | peer_bid_hedge_price = int(peer_bid_price*(1+self.bid_fee_rate)) 108 | peer_ask_hedge_price = int(peer_ask_price*(1-self.ask_fee_rate)) 109 | 110 | buyprice=min(buyprice, peer_bid_hedge_price) - self.bid_price_risk 111 | sellprice=max(sellprice, peer_ask_hedge_price) + self.ask_price_risk 112 | logging.debug("sellprice/buyprice=(%s/%s)" % (sellprice, buyprice)) 113 | 114 | self.buyprice = buyprice 115 | self.sellprice = sellprice 116 | 117 | # Update client balance 118 | self.update_balance() 119 | 120 | # query orders 121 | if self.is_buying(): 122 | for buy_order in self.get_orders('buy'): 123 | logging.debug(buy_order) 124 | result = self.clients[kexchange].get_order(buy_order['id']) 125 | logging.debug(result) 126 | if not result: 127 | logging.warn("get_order buy #%s failed" % (buy_order['id'])) 128 | return 129 | 130 | self.hedge_order(buy_order, result) 131 | 132 | if result['status'] == 'CLOSE' or result['status'] == 'CANCELED': 133 | self.remove_order(buy_order['id']) 134 | else: 135 | current_time = time.time() 136 | if (result['price'] != buyprice) and \ 137 | ((result['price'] > peer_bid_hedge_price) or \ 138 | ( current_time - buy_order['time'] > self.trade_timeout and \ 139 | (result['price'] < bid_price or result['price'] > (bid1_price + 1)))): 140 | logging.info("[TraderBot] cancel last buy trade " + 141 | "occured %.2f seconds ago" % 142 | (current_time - buy_order['time'])) 143 | logging.info("cancel buyprice %s result['price'] = %s[%s]" % (buyprice, result['price'], result['price'] != buyprice)) 144 | 145 | self.cancel_order(kexchange, 'buy', buy_order['id']) 146 | 147 | 148 | if self.is_selling(): 149 | for sell_order in self.get_orders('sell'): 150 | logging.debug(sell_order) 151 | result = self.clients[kexchange].get_order(sell_order['id']) 152 | logging.debug(result) 153 | if not result: 154 | logging.warn("get_order sell #%s failed" % (sell_order['id'])) 155 | return 156 | 157 | self.hedge_order(sell_order, result) 158 | 159 | if result['status'] == 'CLOSE' or result['status'] == 'CANCELED': 160 | self.remove_order(sell_order['id']) 161 | else: 162 | current_time = time.time() 163 | if (result['price'] != sellprice) and \ 164 | ((result['price'] < peer_ask_hedge_price) or \ 165 | (current_time - sell_order['time'] > self.trade_timeout and \ 166 | (result['price'] > ask_price or result['price'] < (ask1_price - 1)))): 167 | logging.info("[TraderBot] cancel last SELL trade " + 168 | "occured %.2f seconds ago" % 169 | (current_time - sell_order['time'])) 170 | logging.info("cancel sellprice %s result['price'] = %s [%s]" % (sellprice, result['price'], result['price'] != sellprice)) 171 | 172 | self.cancel_order(kexchange, 'sell', sell_order['id']) 173 | 174 | # excute trade 175 | if self.buying_len() < config.MAKER_BUY_QUEUE: 176 | self.new_order_notify(kexchange, 'buy') 177 | if self.selling_len() < config.MAKER_SELL_QUEUE: 178 | self.new_order_notify(kexchange, 'sell') 179 | 180 | def update_trade_history(self, time, price, cny, btc): 181 | filename = self.out_dir + self.filename 182 | need_header = False 183 | 184 | if not os.path.exists(filename): 185 | need_header = True 186 | 187 | fp = open(filename, 'a+') 188 | 189 | if need_header: 190 | fp.write("timestamp, price, cny, btc\n") 191 | 192 | fp.write(("%d") % time +','+("%.2f") % price+','+("%.2f") % cny+','+ str(("%.4f") % btc) +'\n') 193 | fp.close() 194 | 195 | def update_balance(self): 196 | for kclient in self.clients: 197 | if kclient == self.exchange: 198 | self.clients[kclient].get_info() 199 | self.cny_balance = self.clients[kclient].cny_balance 200 | self.btc_balance = self.clients[kclient].btc_balance 201 | 202 | self.cny_frozen = self.clients[kclient].cny_frozen 203 | self.btc_frozen = self.clients[kclient].btc_frozen 204 | 205 | cny_abs = abs(self.cny_total - self.cny_balance_total(self.buyprice)) 206 | cny_diff = self.cny_total*0.1 207 | btc_abs = abs(self.btc_total - self.btc_balance_total(self.sellprice)) 208 | btc_diff = self.btc_total*0.1 209 | 210 | self.cny_total = self.cny_balance_total(self.buyprice) 211 | self.btc_total = self.btc_balance_total(self.sellprice) 212 | 213 | if (cny_abs > 5 and cny_abs < cny_diff) or (btc_abs > 0.001 and btc_abs < btc_diff): 214 | logging.debug("update_balance-->") 215 | self.update_trade_history(time.time(), self.buyprice, self.cny_total, self.btc_total) 216 | 217 | logging.debug("cny_balance=%s/%s, btc_balance=%s/%s, total_cny=%0.2f, total_btc=%0.2f", 218 | self.cny_balance, self.cny_frozen, self.btc_balance, self.btc_frozen, 219 | self.cny_balance_total(self.buyprice), self.btc_balance_total(self.sellprice)) 220 | 221 | def cny_balance_total(self, price): 222 | return self.cny_balance + self.cny_frozen+ (self.btc_balance + self.btc_frozen)* price 223 | 224 | def btc_balance_total(self, price): 225 | return self.btc_balance + self.btc_frozen + (self.cny_balance +self.cny_frozen ) / (price*1.0) 226 | 227 | def new_order_notify(self, kexchange, type, maker_only=True, amount=None, price=None): 228 | order = super().new_order(kexchange, type, maker_only, amount, price) 229 | 230 | if order: 231 | # self.notify_msg(order['type'], order['price']) 232 | t = threading.Thread(target = self.notify_msg, args=(order['type'], order['price'],)) 233 | t.start() 234 | logging.info("current has %d threads" % (threading.activeCount() - 1)) 235 | 236 | def begin_opportunity_finder(self, depths): 237 | self.market_maker(depths) 238 | 239 | def end_opportunity_finder(self): 240 | pass 241 | 242 | def opportunity(self, profit, volume, buyprice, kask, sellprice, kbid, perc, weighted_buyprice, weighted_sellprice): 243 | pass 244 | -------------------------------------------------------------------------------- /arbitrage/observers/observer.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class Observer(object, metaclass=abc.ABCMeta): 5 | def __init__(self): 6 | self.is_terminated = False 7 | 8 | def terminate(self): 9 | self.is_terminated = True 10 | 11 | def begin_opportunity_finder(self, depths): 12 | pass 13 | 14 | def end_opportunity_finder(self): 15 | pass 16 | 17 | ## abstract 18 | @abc.abstractmethod 19 | def opportunity(self, profit, volume, buyprice, kask, sellprice, kbid, perc, weighted_buyprice, weighted_sellprice): 20 | pass 21 | -------------------------------------------------------------------------------- /arbitrage/observers/specializedtraderbot.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import config 3 | import time 4 | from .observer import Observer 5 | from private_markets import haobtccny,huobicny,okcoincny 6 | 7 | from .emailer import send_email 8 | 9 | 10 | class SpecializedTraderBot(Observer): 11 | def __init__(self): 12 | self.haobtc = haobtccny.PrivateHaobtcCNY(config.HAOBTC_API_KEY, config.HAOBTC_SECRET_TOKEN) 13 | 14 | self.okcoin = okcoincny.PrivateOkCoinCNY(config.OKCOIN_API_KEY, config.OKCOIN_SECRET_TOKEN) 15 | 16 | self.clients = { 17 | "HaobtcCNY": self.haobtc, 18 | "OkCoinCNY": self.okcoin, 19 | } 20 | self.profit_percentage_thresholds = { # Graph 21 | "HaobtcCNY": {"OkCoinCNY": 3.5}, 22 | "OkCoinCNY": {"HaobtcCNY": 1}, 23 | } 24 | self.trade_wait = 60 * 5 # in seconds 25 | self.last_trade = 0 26 | self.potential_trades = [] 27 | 28 | def begin_opportunity_finder(self, depths): 29 | self.potential_trades = [] 30 | 31 | def end_opportunity_finder(self): 32 | if not self.potential_trades: 33 | return 34 | self.potential_trades.sort(key=lambda x: x[0]) 35 | # Execute only the best (more profitable) 36 | self.execute_trade(*self.potential_trades[0][1:]) 37 | 38 | def get_min_tradeable_volume(self, buyprice, cny_bal, btc_bal): 39 | min1 = float(cny_bal) / ((1. + config.balance_margin) * buyprice) 40 | min2 = float(btc_bal) / (1. + config.balance_margin) 41 | return min(min1, min2) * 0.95 42 | 43 | def update_balance(self): 44 | for kclient in self.clients: 45 | self.clients[kclient].get_info() 46 | 47 | def opportunity(self, profit, volume, buyprice, kask, sellprice, kbid, perc, 48 | weighted_buyprice, weighted_sellprice): 49 | if kask not in self.clients: 50 | logging.warn( 51 | "Can't automate this trade, client not available: %s" % (kask)) 52 | return 53 | if kbid not in self.clients: 54 | logging.warn( 55 | "Can't automate this trade, client not available: %s" % (kbid)) 56 | return 57 | if perc < self.profit_percentage_thresholds[kask][kbid]: 58 | logging.warn("Can't automate this trade, profit=%f is lower than defined threshold %f" 59 | % (perc, self.profit_percentage_thresholds[kask][kbid])) 60 | return 61 | 62 | if perc > 20: # suspicous profit, added after discovering btc-central may send corrupted order book 63 | logging.warn("Profit=%f seems malformed" % (perc, )) 64 | return 65 | 66 | # Update client balance 67 | self.update_balance() 68 | 69 | # maximum volume transaction with current balances 70 | max_volume = self.get_min_tradeable_volume( 71 | buyprice, self.clients[kask].cny_balance, 72 | self.clients[kbid].btc_balance) 73 | volume = min(volume, max_volume, config.max_tx_volume) 74 | if volume < config.min_tx_volume: 75 | logging.warn("Can't automate this trade, minimum volume transaction not reached %f/%f" % (volume, config.min_tx_volume)) 76 | logging.info("Balance on %s: %f CNY - Balance on %s: %f BTC" % (kask, self.clients[kask].cny_balance, kbid, self.clients[kbid].btc_balance)) 77 | return 78 | 79 | current_time = time.time() 80 | if current_time - self.last_trade < self.trade_wait: 81 | logging.warn("Can't automate this trade, last trade occured %s seconds ago" 82 | % (current_time - self.last_trade)) 83 | return 84 | 85 | self.potential_trades.append([profit, volume, kask, kbid, weighted_buyprice, 86 | weighted_sellprice]) 87 | 88 | def execute_trade(self, volume, kask, kbid, weighted_buyprice, weighted_sellprice): 89 | self.last_trade = time.time() 90 | logging.info("Buy @%s %f BTC and sell @%s" % (kask, volume, kbid)) 91 | send_email("Bought @%s %f BTC and sold @%s" % (kask, volume, kbid), 92 | "weighted_buyprice=%f weighted_sellprice=%f" % (weighted_buyprice, weighted_sellprice)) 93 | self.clients[kask].buy(volume) 94 | self.clients[kbid].sell(volume) 95 | -------------------------------------------------------------------------------- /arbitrage/observers/traderbot.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import config 3 | import time 4 | from .observer import Observer 5 | from .emailer import send_email 6 | from fiatconverter import FiatConverter 7 | from private_markets import huobicny,okcoincny,brokercny 8 | import os, time 9 | import sys 10 | import traceback 11 | from .basicbot import BasicBot 12 | 13 | class TraderBot(BasicBot): 14 | def __init__(self): 15 | super().__init__() 16 | 17 | self.clients = { 18 | # "HaobtcCNY": haobtccny.PrivateHaobtcCNY(config.HAOBTC_API_KEY, config.HAOBTC_SECRET_TOKEN), 19 | # "OKCoinCNY": okcoincny.PrivateOkCoinCNY(config.OKCOIN_API_KEY, config.OKCOIN_SECRET_TOKEN), 20 | # "HuobiCNY": huobicny.PrivateHuobiCNY(config.HUOBI_API_KEY, config.HUOBI_SECRET_TOKEN), 21 | # "BrokerCNY": brokercny.PrivateBrokerCNY(), 22 | } 23 | 24 | self.reverse_profit_thresh = config.reverse_profit_thresh 25 | self.reverse_perc_thresh = config.reverse_perc_thresh 26 | self.profit_thresh = config.profit_thresh 27 | self.perc_thresh = config.perc_thresh 28 | self.trade_wait = config.trade_wait # in seconds 29 | self.last_trade = 0 30 | 31 | self.init_btc = {'OKCoinCNY':500, 'HuobiCNY':500} 32 | self.init_cny = {'OKCoinCNY':100, 'HuobiCNY':100} 33 | 34 | self.stage0_percent = config.stage0_percent 35 | self.stage1_percent = config.stage1_percent 36 | self.last_bid_price = 0 37 | self.trend_up = True 38 | 39 | self.hedger = 'BrokerCNY' 40 | 41 | def begin_opportunity_finder(self, depths): 42 | self.potential_trades = [] 43 | 44 | # Update client balance 45 | self.update_balance() 46 | 47 | self.check_order(depths) 48 | 49 | def update_balance(self): 50 | for kclient in self.clients: 51 | self.clients[kclient].get_info() 52 | 53 | def end_opportunity_finder(self): 54 | if not self.potential_trades: 55 | return 56 | self.potential_trades.sort(key=lambda x: x[0]) 57 | # Execute only the best (more profitable) 58 | self.execute_trade(*self.potential_trades[0][1:]) 59 | 60 | def get_min_tradeable_volume(self, buyprice, cny_bal, btc_bal): 61 | min1 = float(cny_bal) * (1. - config.balance_margin) / buyprice 62 | min2 = float(btc_bal) * (1. - config.balance_margin) 63 | 64 | return min(min1, min2) 65 | 66 | def check_order(self, depths): 67 | # update price 68 | 69 | # query orders 70 | if self.is_buying(): 71 | buy_orders = self.get_orders('buy') 72 | buy_orders.sort(key=lambda x: x['price'], reverse=True) 73 | 74 | for buy_order in buy_orders: 75 | logging.debug(buy_order) 76 | result = self.clients[buy_order['market']].get_order(buy_order['id']) 77 | logging.debug (result) 78 | if not result: 79 | logging.warn("get_order buy #%s failed" % (buy_order['id'])) 80 | continue 81 | 82 | if result['status'] == 'CLOSE' or result['status'] == 'CANCELED': 83 | if result['status'] == 'CANCELED': 84 | left_amount = result['amount']- result['deal_size'] 85 | logging.info("cancel ok %s result['price'] = %s, left_amount=%s" % (buy_order['market'], result['price'], left_amount)) 86 | 87 | self.clients[self.hedger].buy(left_amount, result['price']) 88 | 89 | self.remove_order(buy_order['id']) 90 | else: 91 | try: 92 | ask_price = int(depths[buy_order['market']]["asks"][0]['price']) 93 | except Exception as ex: 94 | logging.warn("exception depths:%s" % ex) 95 | traceback.print_exc() 96 | continue 97 | 98 | if abs(result['price']-ask_price) > config.arbitrage_cancel_price_diff: 99 | left_amount = result['amount']- result['deal_size'] 100 | logging.info("Fire:cancel %s ask_price %s result['price'] = %s, left_amount=%s" % (buy_order['market'], ask_price, result['price'], left_amount)) 101 | self.cancel_order(buy_order['market'], 'buy', buy_order['id']) 102 | 103 | if self.is_selling(): 104 | sell_orders = self.get_orders('sell') 105 | sell_orders.sort(key=lambda x: x['price']) 106 | 107 | for sell_order in self.get_orders('sell'): 108 | logging.debug(sell_order) 109 | result = self.clients[sell_order['market']].get_order(sell_order['id']) 110 | logging.debug (result) 111 | if not result: 112 | logging.warn("get_order sell #%s failed" % (sell_order['id'])) 113 | continue 114 | 115 | if result['status'] == 'CLOSE' or result['status'] == 'CANCELED': 116 | if result['status'] == 'CANCELED': 117 | left_amount = result['amount']- result['deal_size'] 118 | logging.info("cancel ok %s result['price'] = %s, left_amount=%s" % (sell_order['market'], result['price'], left_amount)) 119 | 120 | self.clients[self.hedger].sell(left_amount, result['price']) 121 | 122 | self.remove_order(sell_order['id']) 123 | else: 124 | try: 125 | bid_price = int(depths[sell_order['market']]["bids"][0]['price']) 126 | except Exception as ex: 127 | logging.warn("exception depths:%s" % ex) 128 | traceback.print_exc() 129 | continue 130 | 131 | if abs(result['price']-bid_price) > config.arbitrage_cancel_price_diff: 132 | left_amount = result['amount']- result['deal_size'] 133 | 134 | logging.info("Fire:cancel %s bid_price %s result['price'] = %s,left_amount=%s" % (sell_order['market'], bid_price, result['price'], left_amount)) 135 | self.cancel_order(sell_order['market'], 'sell', sell_order['id']) 136 | 137 | def opportunity(self, profit, volume, buyprice, kask, sellprice, kbid, perc, 138 | weighted_buyprice, weighted_sellprice): 139 | if kask not in self.clients: 140 | logging.warn("Can't automate this trade, client not available: %s" % kask) 141 | return 142 | if kbid not in self.clients: 143 | logging.warn("Can't automate this trade, client not available: %s" % kbid) 144 | return 145 | 146 | if self.buying_len() >= config.ARBITRAGER_BUY_QUEUE: 147 | logging.warn("Can't automate this trade, BUY queue is full: %s" % self.buying_len()) 148 | return 149 | 150 | if self.selling_len() >= config.ARBITRAGER_SELL_QUEUE: 151 | logging.warn("Can't automate this trade, SELL queue is full: %s" % self.selling_len()) 152 | return 153 | 154 | arbitrage_max_volume = config.max_tx_volume 155 | if profit < self.reverse_profit_thresh and perc < self.reverse_perc_thresh: 156 | logging.info("Profit or profit percentage(%0.4f/%0.4f) lower than thresholds(%s/%s)" 157 | % (profit, perc, self.reverse_profit_thresh, self.reverse_perc_thresh)) 158 | arbitrage_max_volume = config.reverse_max_tx_volume 159 | 160 | if self.clients[kbid].btc_balance < self.stage0_percent*self.init_btc[kbid] or self.clients[kbid].cny_balance < self.stage0_percent*self.init_cny[kbid]: 161 | logging.info("Buy @%s/%0.2f and sell @%s/%0.2f %0.2f BTC" % (kask, buyprice, kbid, sellprice, volume)) 162 | logging.info("%s %s btc:%s < %s,cny:%s < %s, reverse", self.stage0_percent, kbid, self.clients[kbid].btc_balance, self.stage0_percent*self.init_btc[kbid], self.clients[kbid].cny_balance, self.stage0_percent*self.init_cny[kbid]) 163 | ktemp = kbid 164 | kbid = kask 165 | kask = ktemp 166 | elif self.clients[kask].btc_balance < self.stage1_percent*self.init_btc[kask] or self.clients[kask].cny_balance < self.stage1_percent*self.init_cny[kask]: 167 | arbitrage_max_volume = 0.5*(config.reverse_max_tx_volume+config.max_tx_volume) 168 | logging.info("Buy @%s/%0.2f and sell @%s/%0.2f %0.2f BTC" % (kask, buyprice, kbid, sellprice, volume)) 169 | logging.info("%s %s btc:%s < %s, cny:%s <%s, go on", self.stage1_percent, kask, self.clients[kask].btc_balance, self.stage1_percent*self.init_btc[kask],self.clients[kask].cny_balance, self.stage1_percent*self.init_cny[kask]) 170 | else: 171 | logging.debug("wait for higher") 172 | return 173 | elif profit > self.profit_thresh and perc > self.perc_thresh: 174 | logging.info("Profit or profit percentage(%0.4f/%0.4f) higher than thresholds(%s/%s)" 175 | % (profit, perc, self.profit_thresh, self.perc_thresh)) 176 | arbitrage_max_volume = config.max_tx_volume 177 | else: 178 | logging.debug("Profit or profit percentage(%0.4f/%0.4f) out of scope thresholds(%s~%s/%s~%s)" 179 | % (profit, perc, self.reverse_profit_thresh, self.profit_thresh, self.perc_thresh, self.reverse_perc_thresh)) 180 | return 181 | 182 | if perc > 20: # suspicous profit, added after discovering btc-central may send corrupted order book 183 | logging.warn("Profit=%f seems malformed" % (perc, )) 184 | return 185 | 186 | max_volume = self.get_min_tradeable_volume(buyprice, 187 | self.clients[kask].cny_balance, 188 | self.clients[kbid].btc_balance) 189 | volume = min(volume, max_volume, arbitrage_max_volume) 190 | if volume < config.min_tx_volume: 191 | logging.warn("Can't automate this trade, minimum volume transaction"+ 192 | " not reached %f/%f" % (volume, config.min_tx_volume)) 193 | return 194 | 195 | current_time = time.time() 196 | if current_time - self.last_trade < self.trade_wait: 197 | logging.warn("Can't automate this trade, last trade " + 198 | "occured %.2f seconds ago" % 199 | (current_time - self.last_trade)) 200 | return 201 | 202 | self.potential_trades.append([profit, volume, kask, kbid, 203 | weighted_buyprice, weighted_sellprice, 204 | buyprice, sellprice]) 205 | 206 | def execute_trade(self, volume, kask, kbid, weighted_buyprice, 207 | weighted_sellprice, buyprice, sellprice): 208 | volume = float('%0.2f' % volume) 209 | 210 | if self.clients[kask].cny_balance < max(volume*buyprice*10, 31*buyprice): 211 | logging.warn("%s cny is insufficent" % kask) 212 | return 213 | 214 | if self.clients[kbid].btc_balance < max(volume*10, 31): 215 | logging.warn("%s btc is insufficent" % kbid) 216 | return 217 | 218 | logging.info("Fire:Buy @%s/%0.2f and sell @%s/%0.2f %0.2f BTC" % (kask, buyprice, kbid, sellprice, volume)) 219 | 220 | # update trend 221 | if self.last_bid_price < buyprice: 222 | self.trend_up = True 223 | else: 224 | self.trend_up = False 225 | 226 | logging.info("trend is %s[%s->%s]", "up, buy then sell" if self.trend_up else "down, sell then buy", self.last_bid_price, buyprice) 227 | self.last_bid_price = buyprice 228 | 229 | # trade 230 | if self.trend_up: 231 | result = self.new_order(kask, 'buy', maker_only=False, amount=volume, price=buyprice) 232 | if not result: 233 | logging.warn("Buy @%s %f BTC failed" % (kask, volume)) 234 | return 235 | 236 | self.last_trade = time.time() 237 | 238 | result = self.new_order(kbid, 'sell', maker_only=False, amount= volume, price=sellprice) 239 | if not result: 240 | logging.warn("Sell @%s %f BTC failed" % (kbid, volume)) 241 | result = self.new_order(kask, 'sell', maker_only=False, amount=volume, price=buyprice) 242 | if not result: 243 | logging.warn("2nd sell @%s %f BTC failed" % (kask, volume)) 244 | return 245 | else: 246 | 247 | result = self.new_order(kbid, 'sell', maker_only=False, amount= volume, price=sellprice) 248 | if not result: 249 | logging.warn("Sell @%s %f BTC failed" % (kbid, volume)) 250 | return 251 | 252 | self.last_trade = time.time() 253 | 254 | result = self.new_order(kask, 'buy', maker_only=False, amount=volume, price=buyprice) 255 | if not result: 256 | logging.warn("Buy @%s %f BTC failed" % (kask, volume)) 257 | result = self.new_order(kbid, 'buy', maker_only=False, amount= volume, price=sellprice) 258 | if not result: 259 | logging.warn("2nd buy @%s %f BTC failed" % (kbid, volume)) 260 | return 261 | return 262 | 263 | -------------------------------------------------------------------------------- /arbitrage/observers/traderbotsim.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .traderbot import TraderBot 3 | import json 4 | 5 | 6 | class MockMarket(object): 7 | def __init__(self, name, fee=0, cny_balance=3000., btc_balance=10., 8 | persistent=True): 9 | self.name = name 10 | self.filename = "traderbot-sim-" + name + ".json" 11 | self.cny_balance = cny_balance 12 | self.btc_balance = btc_balance 13 | self.cny_frozen = 0 14 | self.btc_frozen = 0 15 | self.cny_total = 0 16 | self.btc_total = 0 17 | 18 | self.fee = fee 19 | self.persistent = persistent 20 | if self.persistent: 21 | try: 22 | self.load() 23 | except IOError: 24 | pass 25 | 26 | def buy(self, volume, price): 27 | logging.info("execute buy %f BTC @ %f on %s" % 28 | (volume, price, self.name)) 29 | self.cny_balance -= price * volume 30 | self.btc_balance += volume - volume * self.fee 31 | if self.persistent: 32 | self.save() 33 | 34 | def sell(self, volume, price): 35 | logging.info("execute sell %f BTC @ %f on %s" % 36 | (volume, price, self.name)) 37 | self.btc_balance -= volume 38 | self.cny_balance += price * volume - price * volume * self.fee 39 | if self.persistent: 40 | self.save() 41 | 42 | def load(self): 43 | data = json.load(open(self.filename, "r")) 44 | self.cny_balance = data["cny"] 45 | self.btc_balance = data["btc"] 46 | 47 | def save(self): 48 | data = {'cny': self.cny_balance, 'btc': self.btc_balance} 49 | json.dump(data, open(self.filename, "w")) 50 | 51 | def balance_total(self, price): 52 | return self.cny_balance + self.btc_balance * price 53 | 54 | def get_info(self): 55 | pass 56 | 57 | 58 | class TraderBotSim(TraderBot): 59 | def __init__(self): 60 | super().__init__() 61 | 62 | self.kraken = MockMarket("kraken", 0.005, 5000) # 0.5% fee 63 | self.paymium = MockMarket("paymium", 0.005, 5000) # 0.5% fee 64 | self.bitstamp = MockMarket("bitstamp", 0.005, 5000) # 0.5% fee 65 | self.btcc = MockMarket("btcc", 0.005, 5000) # 0.5% fee 66 | self.haobtc = MockMarket("haobtc", 0.002, 5000) # 0.2% fee 67 | self.okcoin = MockMarket("okcoin", 0.000, 5000) # 0.0% fee 68 | self.huobi = MockMarket("huobi", 0.000, 5000) # 0.0% fee 69 | self.broker = MockMarket("broker", 0.000, 5000) # 0.0% fee 70 | 71 | self.clients = { 72 | "KrakenEUR": self.kraken, 73 | "PaymiumEUR": self.paymium, 74 | "BitstampUSD": self.bitstamp, 75 | "BTCCCNY": self.btcc, 76 | "HaobtcCNY": self.haobtc, 77 | "OKCoinCNY": self.okcoin, 78 | "HuobiCNY": self.huobi, 79 | "BrokerCNY": self.broker, 80 | } 81 | 82 | self.profit_thresh = 0.1 # in CNY 83 | self.perc_thresh = 0.01 # in % 84 | self.trade_wait = 60 85 | self.last_trade = 0 86 | 87 | def total_balance(self, price): 88 | market_balances = [i.balance_total( 89 | price) for i in set(self.clients.values())] 90 | return sum(market_balances) 91 | 92 | def total_cny_balance(self): 93 | return sum([i.cny_balance for i in set(self.clients.values())]) 94 | 95 | def total_usd_balance(self): 96 | return sum([i.usd_balance for i in set(self.clients.values())]) 97 | 98 | def total_btc_balance(self): 99 | return sum([i.btc_balance for i in set(self.clients.values())]) 100 | 101 | 102 | if __name__ == "__main__": 103 | t = TraderBotSim() 104 | print("Total BTC: %f" % t.total_btc_balance()) 105 | print("Total CNY: %f" % t.total_cny_balance()) 106 | print("Total USD: %f" % t.total_usd_balance()) 107 | -------------------------------------------------------------------------------- /arbitrage/observers/xmppmessager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import config 3 | import time 4 | from sleekxmpp import ClientXMPP 5 | from sleekxmpp.exceptions import IqError, IqTimeout 6 | from .observer import Observer 7 | 8 | 9 | class MyXMPPClient(ClientXMPP): 10 | def __init__(self): 11 | logger = logging.getLogger("sleekxmpp") 12 | logger.setLevel(logging.ERROR) 13 | ClientXMPP.__init__(self, config.xmpp_jid, config.xmpp_password) 14 | self.add_event_handler("session_start", self.session_start) 15 | self.add_event_handler("message", self.message) 16 | self.connect() 17 | self.process(block=False) 18 | 19 | def session_start(self, event): 20 | self.send_presence() 21 | self.get_roster() 22 | 23 | def msend_message(self, message): 24 | logging.debug('Sending XMPP message: "%s" to %s' % (message, 25 | config.xmpp_to)) 26 | self.send_message(mto=config.xmpp_to, mbody=message, mtype='chat') 27 | 28 | def message(self, msg): 29 | # TODO: Use this to control / re-config 30 | pass # msg.reply("%(body)s" % msg).send() 31 | 32 | class XmppMessager(Observer): 33 | def __init__(self): 34 | self.xmppclient = MyXMPPClient() 35 | 36 | def opportunity(self, profit, volume, buyprice, kask, sellprice, kbid, perc, 37 | weighted_buyprice, weighted_sellprice): 38 | if profit > config.profit_thresh and perc > config.perc_thresh: 39 | message = "profit: %f CNY with volume: %f BTC - buy at %.4f (%s) sell at %.4f (%s) ~%.2f%%" % (profit, volume, buyprice, kask, sellprice, kbid, perc) 40 | self.xmppclient.msend_message(message) 41 | -------------------------------------------------------------------------------- /arbitrage/private_markets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artooze/crypto-arbitrager/49aaac9255953cb6c36ab16b09d024afd0dd0935/arbitrage/private_markets/__init__.py -------------------------------------------------------------------------------- /arbitrage/private_markets/bitfinex_bch_btc.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017, Philsong 2 | 3 | from .market import Market, TradeException 4 | import time 5 | import base64 6 | import hmac 7 | import urllib.request 8 | import urllib.parse 9 | import urllib.error 10 | import hashlib 11 | import sys 12 | import json 13 | import config 14 | import logging 15 | import bitfinex 16 | import traceback 17 | 18 | # python3 arbitrage/arbitrage.py -m Bitfinex_BCH_BTC get-balance 19 | 20 | class PrivateBitfinex_BCH_BTC(Market): 21 | def __init__(self, api_key = None, api_secret = None): 22 | super().__init__() 23 | 24 | self.trade_client = bitfinex.TradeClient( 25 | api_key if api_key else config.Bitfinex_API_KEY, 26 | api_secret if api_secret else config.Bitfinex_SECRET_TOKEN) 27 | 28 | self.currency = "BTC" 29 | self.symbol = 'bchbtc' 30 | 31 | self.get_info() 32 | 33 | def _buy(self, amount, price): 34 | """Create a buy limit order""" 35 | print("buy limit...") 36 | return 37 | res = self.trade_client.place_order( 38 | amount, 39 | price, 40 | 'buy', 41 | 'exchange limit', 42 | symbol=self.symbol) 43 | return res['order_id'] 44 | 45 | def _sell(self, amount, price): 46 | """Create a sell limit order""" 47 | print("sell limit...") 48 | return 49 | res = self.trade_client.place_order( 50 | amount, 51 | price, 52 | 'sell', 53 | 'exchange limit', 54 | symbol=self.symbol) 55 | return res['order_id'] 56 | 57 | def get_info(self): 58 | """Get balance""" 59 | res = self.trade_client.balances() 60 | print("req balances response:", res) 61 | 62 | for entry in res: 63 | if entry['type'] != 'exchange': 64 | continue 65 | 66 | currency = entry['currency'].upper() 67 | if currency not in ( 68 | 'BTC', 'BCH'): 69 | continue 70 | 71 | if currency == 'BCH': 72 | self.bch_available = entry['available'] 73 | self.bch_amount = entry['amount'] 74 | 75 | elif currency == 'BTC': 76 | self.btc_available = entry['available'] 77 | self.btc_amount = entry['amount'] 78 | 79 | -------------------------------------------------------------------------------- /arbitrage/private_markets/bitstampusd.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013, Maxime Biais 2 | 3 | from .market import Market, TradeException 4 | import time 5 | import base64 6 | import hmac 7 | import urllib.request 8 | import urllib.parse 9 | import urllib.error 10 | import hashlib 11 | import sys 12 | import json 13 | import config 14 | 15 | 16 | class PrivateBitstampUSD(Market): 17 | balance_url = "https://www.bitstamp.net/api/balance/" 18 | buy_url = "https://www.bitstamp.net/api/buy/" 19 | sell_url = "https://www.bitstamp.net/api/sell/" 20 | 21 | def __init__(self): 22 | super().__init__() 23 | self.username = config.bitstamp_username 24 | self.password = config.bitstamp_password 25 | self.currency = "USD" 26 | self.get_info() 27 | 28 | def _send_request(self, url, params={}, extra_headers=None): 29 | headers = { 30 | 'Content-type': 'application/json', 31 | 'Accept': 'application/json, text/javascript, */*; q=0.01', 32 | 'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' 33 | } 34 | if extra_headers is not None: 35 | for k, v in extra_headers.items(): 36 | headers[k] = v 37 | 38 | params['user'] = self.username 39 | params['password'] = self.password 40 | postdata = urllib.parse.urlencode(params).encode("utf-8") 41 | req = urllib.request.Request(url, postdata, headers=headers) 42 | response = urllib.request.urlopen(req) 43 | code = response.getcode() 44 | if code == 200: 45 | jsonstr = response.read().decode('utf-8') 46 | return json.loads(jsonstr) 47 | return None 48 | 49 | def _buy(self, amount, price): 50 | """Create a buy limit order""" 51 | params = {"amount": amount, "price": price} 52 | response = self._send_request(self.buy_url, params) 53 | if "error" in response: 54 | raise TradeException(response["error"]) 55 | 56 | def _sell(self, amount, price): 57 | """Create a sell limit order""" 58 | params = {"amount": amount, "price": price} 59 | response = self._send_request(self.sell_url, params) 60 | if "error" in response: 61 | raise TradeException(response["error"]) 62 | 63 | def get_info(self): 64 | """Get balance""" 65 | response = self._send_request(self.balance_url) 66 | if response: 67 | self.btc_balance = float(response["btc_available"]) 68 | self.usd_balance = float(response["usd_available"]) 69 | self.cny_balance = self.fc.convert(self.usd_balance, "USD", "CNY") 70 | # todo: 71 | self.btc_frozen = 0. 72 | self.cny_frozen = 0. 73 | -------------------------------------------------------------------------------- /arbitrage/private_markets/bitstarcny.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016, Philsong 2 | 3 | from .market import Market, TradeException 4 | import time 5 | import base64 6 | import hmac 7 | import urllib.request 8 | import urllib.parse 9 | import urllib.error 10 | import hashlib 11 | import sys 12 | import json 13 | import config 14 | from lib.bitstar_sdk import ApiClient 15 | import logging 16 | 17 | class PrivateBitstarCNY(Market): 18 | def __init__(self, API_KEY = None, API_SECRET = None): 19 | super().__init__() 20 | 21 | self.market = ApiClient(API_KEY, API_SECRET) 22 | 23 | self.currency = "CNY" 24 | self.get_info() 25 | 26 | def _trade(self, amount, price, is_long=True): 27 | """Create a buy limit order""" 28 | tradeType = 1 29 | # 1 开多  2 开空  3 平多  4 平空 30 | if is_long: 31 | tradeType = 2 32 | 33 | try: 34 | cny_amount = (amount*price)//100*100 35 | trade = client.trade('swap-btc-cny', tradeType, decimal.Decimal(price), cny_amount) 36 | except expression as identifier: 37 | pass 38 | 39 | if trade and trade['result'] == 0: 40 | return trade['orderid'] 41 | 42 | return False 43 | 44 | def _buy(self, amount, price): 45 | return self._trade(amount, price, is_long=True) 46 | 47 | def _sell(self, amount, price): 48 | """Create a sell limit order""" 49 | return self._trade(amount, price, is_long=False) 50 | 51 | 52 | def _get_order(self, order_id): 53 | order_info = self.market.order_info('swap-btc-cny', order_id) 54 | 55 | if not response: 56 | return response 57 | 58 | if "error_code" in response: 59 | logging.warn (response) 60 | return False 61 | 62 | order = response['orders'][0] 63 | resp = {} 64 | resp['order_id'] = order['order_id'] 65 | resp['amount'] = order['amount'] 66 | resp['price'] = order['price'] 67 | resp['deal_size'] = order['deal_amount'] 68 | resp['avg_price'] = order['avg_price'] 69 | 70 | status = order['status'] 71 | if status == -1: 72 | resp['status'] = 'CANCELED' 73 | elif status == 2: 74 | resp['status'] = 'CLOSE' 75 | else: 76 | resp['status'] = 'OPEN' 77 | return resp 78 | 79 | def _cancel_order(self, order_id): 80 | response = self.market.cancel(order_id) 81 | 82 | if not response: 83 | return response 84 | 85 | if response and "error_code" in response: 86 | logging.warn (response) 87 | return False 88 | 89 | if response['result'] == True: 90 | return True 91 | else: 92 | return False 93 | 94 | def get_info(self): 95 | """Get balance""" 96 | response = self.market.accountInfo() 97 | if response: 98 | if "error_code" in response: 99 | logging.warn(response) 100 | return False 101 | else: 102 | self.btc_balance = float(response['info']['funds']['free']['btc']) 103 | self.cny_balance = float(response['info']['funds']['free']['cny']) 104 | self.btc_frozen = float(response['info']['funds']['freezed']['btc']) 105 | self.cny_frozen = float(response['info']['funds']['freezed']['cny']) 106 | return response 107 | -------------------------------------------------------------------------------- /arbitrage/private_markets/brokercny.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016, Philsong 2 | 3 | from .market import Market, TradeException 4 | import time 5 | import base64 6 | import hmac 7 | import urllib.request 8 | import urllib.parse 9 | import urllib.error 10 | import hashlib 11 | import sys 12 | import json 13 | import config 14 | import logging 15 | import lib.broker_api as exchange_api 16 | import traceback 17 | 18 | class PrivateBrokerCNY(Market): 19 | def __init__(self): 20 | super().__init__() 21 | exchange_api.init_broker() 22 | 23 | self.currency = "CNY" 24 | self.get_info() 25 | self.client_id = 0 26 | 27 | self.filename = "broker-clientid.json" 28 | try: 29 | self.load() 30 | except IOError: 31 | logging.warn("load client id failed!") 32 | pass 33 | 34 | def load(self): 35 | data = json.load(open(self.filename, "r")) 36 | self.client_id = data["client_id"] 37 | 38 | def save(self): 39 | data = {'client_id': self.client_id} 40 | json.dump(data, open(self.filename, "w")) 41 | 42 | def _buy(self, amount, price, client_id=None): 43 | """Create a buy limit order""" 44 | if not client_id: 45 | self.client_id+=1 46 | client_id = self.client_id 47 | self.save() 48 | 49 | exchange_api.exchange_buy(client_id, amount, price) 50 | 51 | def _sell(self, amount, price, client_id=None): 52 | """Create a sell limit order""" 53 | if not client_id: 54 | self.client_id+=1 55 | client_id = self.client_id 56 | self.save() 57 | 58 | exchange_api.exchange_sell(client_id, amount, price) 59 | 60 | def get_info(self): 61 | """Get balance""" 62 | try: 63 | accounts = exchange_api.exchange_get_account() 64 | except Exception as e: 65 | traceback.print_exc() 66 | exchange_api.init_broker() 67 | return 68 | 69 | if accounts: 70 | self.cny_balance = 0 71 | self.btc_balance = 0 72 | self.cny_frozen = 0 73 | self.btc_frozen = 0 74 | 75 | for account in accounts: 76 | self.btc_balance += account.available_btc 77 | self.cny_balance += account.available_cny 78 | self.btc_frozen += account.frozen_cny 79 | self.cny_frozen += account.frozen_btc 80 | -------------------------------------------------------------------------------- /arbitrage/private_markets/btccprocny.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013, Maxime Biais 2 | 3 | from .market import Market, TradeException 4 | import time 5 | import base64 6 | import hmac 7 | import urllib.request 8 | import urllib.parse 9 | import urllib.error 10 | import hashlib 11 | import sys 12 | import json 13 | import config 14 | from lib.helpers import * 15 | 16 | 17 | class PrivateBtccProCNY(Market): 18 | balance_url = "http://localhost:8080/btcc/account" 19 | buy_url = "http://localhost:8080/btcc/buy" 20 | sell_url = "http://localhost:8080/btcc/sell" 21 | 22 | def __init__(self): 23 | super().__init__() 24 | self.currency = "CNY" 25 | self.get_info() 26 | 27 | def _buy(self, amount, price): 28 | """Create a buy limit order""" 29 | print("buy...") 30 | params = {"amount": amount, "price": price} 31 | return requestPost(self.buy_url, params) 32 | 33 | response = httpPost(self.buy_url, params) 34 | if not response: 35 | raise TradeException("buy failed") 36 | 37 | def _sell(self, amount, price): 38 | """Create a sell limit order""" 39 | print("sell...") 40 | 41 | params = {"amount": amount, "price": price} 42 | return requestPost(self.sell_url, params) 43 | 44 | response = httpPost(self.sell_url, params) 45 | if not response: 46 | raise TradeException("sell failed") 47 | 48 | def get_info(self): 49 | """Get balance""" 50 | response = requestGet(self.balance_url) 51 | # print("btccpro get_info response:", response) 52 | if response: 53 | # self.btc_balance = float(response["UsableMargin"]) 54 | self.cny_balance = float(response["UsableMargin"]) 55 | # todo: 56 | self.sell_frozen = float(response["TotalSellSize1"]) 57 | self.buy_frozen = float(response["TotalBuySize1"]) 58 | -------------------------------------------------------------------------------- /arbitrage/private_markets/haobtccny.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016, Philsong 2 | 3 | from .market import Market, TradeException 4 | import time 5 | import base64 6 | import hmac 7 | import urllib.request 8 | import urllib.parse 9 | import urllib.error 10 | import hashlib 11 | import sys 12 | import json 13 | import config 14 | from lib.exchange import exchange 15 | from lib.settings import HAOBTC_API_URL 16 | import logging 17 | 18 | class PrivateHaobtcCNY(Market): 19 | def __init__(self, HAOBTC_API_KEY=None, HAOBTC_SECRET_TOKEN=None): 20 | super().__init__() 21 | if HAOBTC_API_KEY == None: 22 | HAOBTC_API_KEY = config.HAOBTC_API_KEY 23 | HAOBTC_SECRET_TOKEN = config.HAOBTC_SECRET_TOKEN 24 | self.market = exchange(HAOBTC_API_URL, HAOBTC_API_KEY, HAOBTC_SECRET_TOKEN, 'haobtc') 25 | 26 | self.currency = "CNY" 27 | self.get_info() 28 | 29 | def _buy(self, amount, price): 30 | """Create a buy limit order""" 31 | response = self.market.buy(amount, price) 32 | if response and "code" in response: 33 | logging.warn (response) 34 | return False 35 | if not response: 36 | return response 37 | 38 | return response['order_id'] 39 | 40 | def _sell(self, amount, price): 41 | """Create a sell limit order""" 42 | response = self.market.sell(amount, price) 43 | if response and "code" in response: 44 | logging.warn (response) 45 | return False 46 | if not response: 47 | return response 48 | return response['order_id'] 49 | 50 | def _buy_maker(self, amount, price): 51 | response = self.market.bidMakerOnly(amount, price) 52 | if response and "code" in response: 53 | logging.warn (response) 54 | return False 55 | if not response: 56 | return response 57 | 58 | return response['order_id'] 59 | 60 | def _sell_maker(self, amount, price): 61 | response = self.market.askMakerOnly(amount, price) 62 | if response and "code" in response: 63 | logging.warn (response) 64 | return False 65 | if not response: 66 | return response 67 | 68 | return response['order_id'] 69 | 70 | def _get_order(self, order_id): 71 | response = self.market.orderInfo(order_id) 72 | if not response: 73 | return response 74 | 75 | if "code" in response: 76 | logging.warn (response) 77 | return False 78 | 79 | return response 80 | 81 | def _cancel_order(self, order_id): 82 | response = self.market.cancel(order_id) 83 | 84 | if not response: 85 | return response 86 | 87 | if response and "code" in response: 88 | logging.warn (response) 89 | return False 90 | 91 | resp_order_id = response['order_id'] 92 | if resp_order_id == -1: 93 | logging.warn("cancel order #%s failed, %s" % (order_id, resp_order_id)) 94 | return False 95 | else: 96 | logging.debug("Canceled order #%s ok" % (order_id)) 97 | return True 98 | return True 99 | 100 | def _cancel_all(self): 101 | response = self.market.cancelAll() 102 | if response and "code" in response: 103 | logging.warn (response) 104 | return False 105 | return response 106 | 107 | def get_info(self): 108 | """Get balance""" 109 | response = self.market.accountInfo() 110 | if response: 111 | if "code" in response: 112 | logging.warn("get_info failed %s", response) 113 | return False 114 | else: 115 | self.btc_balance = float(response["exchange_btc"]) 116 | self.cny_balance = float(response["exchange_cny"]) 117 | self.btc_frozen = float(response["exchange_frozen_btc"]) 118 | self.cny_frozen = float(response["exchange_frozen_cny"]) 119 | 120 | return response 121 | -------------------------------------------------------------------------------- /arbitrage/private_markets/huobicny.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*-s = u’示例’ 2 | # Copyright (C) 2016, Philsong 3 | 4 | from .market import Market, TradeException 5 | import time 6 | import base64 7 | import hmac 8 | import urllib.request 9 | import urllib.parse 10 | import urllib.error 11 | import hashlib 12 | import sys 13 | import json 14 | from lib.exchange import exchange 15 | from lib.settings import HUOBI_API_URL 16 | import sys 17 | import traceback 18 | import config 19 | import logging 20 | 21 | class PrivateHuobiCNY(Market): 22 | def __init__(self,HUOBI_API_KEY=None, HUOBI_SECRET_TOKEN=None): 23 | super().__init__() 24 | if HUOBI_API_KEY == None: 25 | HUOBI_API_KEY = config.HUOBI_API_KEY 26 | HUOBI_SECRET_TOKEN = config.HUOBI_SECRET_TOKEN 27 | self.market = exchange(HUOBI_API_URL, HUOBI_API_KEY, HUOBI_SECRET_TOKEN, 'huobi') 28 | self.currency = "CNY" 29 | self.get_info() 30 | 31 | def _buy(self, amount, price): 32 | """Create a buy limit order""" 33 | response = self.market.buy(amount, price) 34 | if response and "code" in response: 35 | logging.warn("buy ex:%s", response) 36 | return False 37 | 38 | if not response: 39 | return response 40 | 41 | return response['id'] 42 | 43 | def _sell(self, amount, price): 44 | """Create a sell limit order""" 45 | response = self.market.sell(amount, price) 46 | if response and "code" in response: 47 | logging.warn("sell ex:%s", response) 48 | return False 49 | if not response: 50 | return response 51 | 52 | return response['id'] 53 | 54 | 55 | def _get_order(self, order_id): 56 | try: 57 | response = self.market.orderInfo(order_id) 58 | except Exception as ex: 59 | logging.warn("orderInfo failed :%s" % ex) 60 | traceback.print_exc() 61 | return False 62 | 63 | if not response: 64 | return response 65 | 66 | if "code" in response: 67 | logging.warn (response) 68 | return False 69 | 70 | resp = {} 71 | resp['order_id'] = response['id'] 72 | resp['amount'] = float(response['order_amount']) 73 | resp['price'] = float(response['order_price']) 74 | resp['deal_size'] = float(response['processed_amount']) 75 | resp['avg_price'] = float(response['processed_price']) 76 | 77 | status = response['status'] 78 | if status == 3 or status == 6: 79 | resp['status'] = 'CANCELED' 80 | elif status == 2: 81 | resp['status'] = 'CLOSE' 82 | else: 83 | resp['status'] = 'OPEN' 84 | return resp 85 | 86 | def _cancel_order(self, order_id): 87 | try: 88 | response = self.market.cancel(order_id) 89 | except Exception as ex: 90 | logging.warn("cancel failed :%s" % ex) 91 | traceback.print_exc() 92 | return False 93 | 94 | if not response: 95 | return response 96 | if "code" in response: 97 | logging.warn ('%s', str(response)) 98 | return False 99 | if response['result'] == 'success': 100 | return True 101 | return False 102 | 103 | def get_info(self): 104 | """Get balance""" 105 | try: 106 | response = self.market.accountInfo() 107 | if response and "code" in response: 108 | logging.warn(response) 109 | return False 110 | raise TradeException(response["message"]) 111 | if response: 112 | self.btc_balance = float(response["available_btc_display"]) 113 | self.cny_balance = float(response["available_cny_display"]) 114 | self.btc_frozen = float(response["frozen_btc_display"]) 115 | self.cny_frozen = float(response["frozen_cny_display"]) 116 | except Exception as ex: 117 | logging.warn("get_info failed :%s" % ex) 118 | traceback.print_exc() 119 | 120 | return False 121 | 122 | -------------------------------------------------------------------------------- /arbitrage/private_markets/market.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013, Maxime Biais 2 | 3 | import logging 4 | from fiatconverter import FiatConverter 5 | 6 | class TradeException(Exception): 7 | pass 8 | 9 | class Market: 10 | def __init__(self): 11 | self.name = self.__class__.__name__ 12 | self.btc_balance = 0. 13 | self.bch_balance = 0. 14 | self.usd_balance = 0. 15 | self.cny_balance = 0. 16 | self.btc_frozen = 0. 17 | self.bch_frozen = 0. 18 | self.usd_frozen = 0. 19 | self.cny_frozen = 0. 20 | self.fc = FiatConverter() 21 | self.market = None 22 | 23 | def __str__(self): 24 | return "%s: %s" % (self.name, str({"cny_balance": self.cny_balance, 25 | "btc_balance": self.btc_balance, 26 | "cny_frozen": self.cny_frozen, 27 | "btc_frozen": self.btc_frozen})) 28 | 29 | def buy(self, amount, price, client_id=None): 30 | """Orders are always priced in CNY""" 31 | local_currency_price = self.fc.convert(price, "CNY", self.currency) 32 | logging.verbose("Buy %f BTC at %f %s (%f CNY) @%s" % (amount, 33 | local_currency_price, self.currency, price, self.name)) 34 | if client_id: 35 | return self._buy(amount, local_currency_price, client_id) 36 | else: 37 | return self._buy(amount, local_currency_price) 38 | 39 | 40 | def sell(self, amount, price, client_id=None): 41 | """Orders are always priced in CNY""" 42 | local_currency_price = self.fc.convert(price, "CNY", self.currency) 43 | logging.verbose("Sell %f BTC at %f %s (%f CNY) @%s" % (amount, 44 | local_currency_price, self.currency, price, self.name)) 45 | if client_id: 46 | return self._sell(amount, local_currency_price, client_id) 47 | else: 48 | return self._sell(amount, local_currency_price) 49 | 50 | 51 | def buy_maker(self, amount, price): 52 | """Orders are always priced in CNY""" 53 | 54 | local_currency_price = self.fc.convert(price, "CNY", self.currency) 55 | local_currency_price = int(local_currency_price) 56 | logging.verbose("Buy maker %f BTC at %d %s (%d CNY) @%s" % (amount, 57 | local_currency_price, self.currency, price, self.name)) 58 | 59 | return self._buy_maker(amount, local_currency_price) 60 | 61 | 62 | def sell_maker(self, amount, price): 63 | """Orders are always priced in CNY""" 64 | local_currency_price = self.fc.convert(price, "CNY", self.currency) 65 | local_currency_price = int(local_currency_price) 66 | 67 | logging.verbose("Sell maker %f BTC at %d %s (%d CNY) @%s" % (amount, 68 | local_currency_price, self.currency, price, self.name)) 69 | 70 | return self._sell_maker(amount, local_currency_price) 71 | 72 | def get_order(self, order_id): 73 | return self._get_order(order_id) 74 | 75 | def cancel_order(self, order_id): 76 | return self._cancel_order(order_id) 77 | 78 | def cancel_all(self): 79 | return self._cancel_all() 80 | 81 | 82 | def _buy(self, amount, price): 83 | raise NotImplementedError("%s.buy(self, amount, price)" % self.name) 84 | 85 | def _sell(self, amount, price): 86 | raise NotImplementedError("%s.sell(self, amount, price)" % self.name) 87 | 88 | def _buy_maker(self, amount, price): 89 | raise NotImplementedError("%s.buy_maker(self, amount, price)" % self.name) 90 | 91 | def _sell_maker(self, amount, price): 92 | raise NotImplementedError("%s.sell_maker(self, amount, price)" % self.name) 93 | 94 | 95 | def _get_order(self, order_id): 96 | raise NotImplementedError("%s.get_order(self, order_id)" % self.name) 97 | 98 | def _cancel_order(self, order_id): 99 | raise NotImplementedError("%s.cancel_order(self, order_id)" % self.name) 100 | 101 | def _cancel_all(self): 102 | raise NotImplementedError("%s.cancel_all(self)" % self.name) 103 | 104 | def deposit(self): 105 | raise NotImplementedError("%s.sell(self, amount, price)" % self.name) 106 | 107 | def withdraw(self, amount, address): 108 | raise NotImplementedError("%s.sell(self, amount, price)" % self.name) 109 | 110 | def get_info(self): 111 | raise NotImplementedError("%s.sell(self, amount, price)" % self.name) 112 | -------------------------------------------------------------------------------- /arbitrage/private_markets/okcoincny.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016, Philsong 2 | 3 | from .market import Market, TradeException 4 | import time 5 | import base64 6 | import hmac 7 | import urllib.request 8 | import urllib.parse 9 | import urllib.error 10 | import hashlib 11 | import sys 12 | import json 13 | import config 14 | from lib.exchange import exchange 15 | from lib.settings import OKCOIN_API_URL 16 | import logging 17 | 18 | class PrivateOkCoinCNY(Market): 19 | def __init__(self, OKCOIN_API_KEY = None, OKCOIN_SECRET_TOKEN = None): 20 | super().__init__() 21 | if OKCOIN_API_KEY == None: 22 | OKCOIN_API_KEY = config.OKCOIN_API_KEY 23 | OKCOIN_SECRET_TOKEN = config.OKCOIN_SECRET_TOKEN 24 | self.market = exchange(OKCOIN_API_URL, OKCOIN_API_KEY, OKCOIN_SECRET_TOKEN, 'okcoin') 25 | 26 | self.currency = "CNY" 27 | self.get_info() 28 | 29 | def _buy(self, amount, price): 30 | """Create a buy limit order""" 31 | response = self.market.buy(amount, price) 32 | if response and "error_code" in response: 33 | logging.warn(response) 34 | return False 35 | if not response: 36 | return response 37 | 38 | return response['order_id'] 39 | 40 | def _sell(self, amount, price): 41 | """Create a sell limit order""" 42 | response = self.market.sell(amount, price) 43 | if response and "error_code" in response: 44 | logging.warn(response) 45 | return False 46 | 47 | if not response: 48 | return response 49 | 50 | return response['order_id'] 51 | 52 | def _get_order(self, order_id): 53 | response = self.market.orderInfo(order_id) 54 | 55 | if not response: 56 | return response 57 | 58 | if "error_code" in response: 59 | logging.warn (response) 60 | return False 61 | 62 | order = response['orders'][0] 63 | resp = {} 64 | resp['order_id'] = order['order_id'] 65 | resp['amount'] = order['amount'] 66 | resp['price'] = order['price'] 67 | resp['deal_size'] = order['deal_amount'] 68 | resp['avg_price'] = order['avg_price'] 69 | 70 | status = order['status'] 71 | if status == -1: 72 | resp['status'] = 'CANCELED' 73 | elif status == 2: 74 | resp['status'] = 'CLOSE' 75 | else: 76 | resp['status'] = 'OPEN' 77 | return resp 78 | 79 | def _cancel_order(self, order_id): 80 | response = self.market.cancel(order_id) 81 | 82 | if not response: 83 | return response 84 | 85 | if response and "error_code" in response: 86 | logging.warn (response) 87 | return False 88 | 89 | if response['result'] == True: 90 | return True 91 | else: 92 | return False 93 | 94 | def get_info(self): 95 | """Get balance""" 96 | response = self.market.accountInfo() 97 | if response: 98 | if "error_code" in response: 99 | logging.warn(response) 100 | return False 101 | else: 102 | self.btc_balance = float(response['info']['funds']['free']['btc']) 103 | self.cny_balance = float(response['info']['funds']['free']['cny']) 104 | self.btc_frozen = float(response['info']['funds']['freezed']['btc']) 105 | self.cny_frozen = float(response['info']['funds']['freezed']['cny']) 106 | return response 107 | -------------------------------------------------------------------------------- /arbitrage/private_markets/paymium.py: -------------------------------------------------------------------------------- 1 | from .market import Market 2 | import time 3 | import base64 4 | import hmac 5 | import urllib.request 6 | import urllib.parse 7 | import urllib.error 8 | import urllib.request 9 | import urllib.error 10 | import urllib.parse 11 | import hashlib 12 | import sys 13 | import json 14 | import config 15 | 16 | 17 | class PrivatePaymium(Market): 18 | balance_url = "https://paymium.com/api/v1/balances/" 19 | trade_url = "https://paymium.com/api/v1/trade_orders/" 20 | withdraw_url = "https://paymium.com/api/v1/transfers/send_bitcoins/" 21 | 22 | def __init__(self): 23 | # FIXME: update this file when bitcoin central re-opens 24 | raise Exception("Paymium is closed") 25 | super().__init__() 26 | self.username = config.paymium_username 27 | self.password = config.paymium_password 28 | self.currency = "EUR" 29 | self.get_info() 30 | 31 | def _create_nonce(self): 32 | return int(time.time() * 1000000) 33 | 34 | def _send_request(self, url, params=[], extra_headers=None): 35 | headers = { 36 | 'Content-type': 'application/json', 37 | 'Accept': 'application/json, text/javascript, */*; q=0.01', 38 | 'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' 39 | } 40 | if extra_headers is not None: 41 | for k, v in extra_headers.items(): 42 | headers[k] = v 43 | 44 | req = None 45 | if params: 46 | req = urllib.request.Request( 47 | url, json.dumps(params), headers=headers) 48 | else: 49 | req = urllib.request.Request(url, headers=headers) 50 | userpass = '%s:%s' % (self.username, self.password) 51 | base64string = base64.b64encode(bytes( 52 | userpass, 'utf-8')).decode('ascii') 53 | req.add_header("Authorization", "Basic %s" % base64string) 54 | response = urllib.request.urlopen(req) 55 | code = response.getcode() 56 | if code == 200: 57 | jsonstr = response.read().decode('utf-8') 58 | return json.loads(jsonstr) 59 | return None 60 | 61 | def trade(self, amount, ttype, price=None): 62 | # params = [("amount", amount), ("currency", self.currency), ("type", 63 | # ttype)] 64 | params = {"amount": amount, "currency": self.currency, "type": ttype} 65 | if price: 66 | params["price"] = price 67 | response = self._send_request(self.trade_url, params) 68 | return response 69 | 70 | def buy(self, amount, price=None): 71 | response = self.trade(amount, "buy", price) 72 | 73 | def sell(self, amount, price=None): 74 | response = self.trade(amount, "sell", price) 75 | print(response) 76 | 77 | def withdraw(self, amount, address): 78 | params = {"amount": amount, "address": address} 79 | response = self._send_request(self.trade_url, params) 80 | return response 81 | 82 | def deposit(self): 83 | return config.paymium_address 84 | 85 | def get_info(self): 86 | response = self._send_request(self.balance_url) 87 | if response: 88 | self.btc_balance = response["BTC"] 89 | self.eur_balance = response["EUR"] 90 | self.usd_balance = self.fc.convert(self.eur_balance, "EUR", "USD") 91 | self.cny_balance = self.fc.convert(self.eur_balance, "EUR", "CNY") 92 | 93 | if __name__ == "__main__": 94 | market = PrivatePaymium() 95 | market.get_info() 96 | print(market) 97 | -------------------------------------------------------------------------------- /arbitrage/public_markets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artooze/crypto-arbitrager/49aaac9255953cb6c36ab16b09d024afd0dd0935/arbitrage/public_markets/__init__.py -------------------------------------------------------------------------------- /arbitrage/public_markets/_bitfinex.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017, Philsong 2 | 3 | import logging 4 | import requests 5 | from .market import Market 6 | 7 | # https://api.bitfinex.com/v1/symbols_details 8 | # { 9 | # "pair": "bchbtc", 10 | # "price_precision": 5, 11 | # "initial_margin": "30.0", 12 | # "minimum_margin": "15.0", 13 | # "maximum_order_size": "2000.0", 14 | # "minimum_order_size": "0.001", 15 | # "expiration": "NA" 16 | # }, 17 | 18 | class Bitfinex(Market): 19 | def __init__(self, base_currency, market_currency, pair_code): 20 | super().__init__(base_currency, market_currency, pair_code) 21 | 22 | def update_depth(self): 23 | url = 'https://api.bitfinex.com/v1/book/%s' % self.pair_code 24 | response = requests.request("GET", url, timeout=self.request_timeout) 25 | raw_depth = response.json() 26 | 27 | self.depth = self.format_depth(raw_depth) 28 | 29 | # override method 30 | def sort_and_format(self, l, reverse=False): 31 | l.sort(key=lambda x: float(x['price']), reverse=reverse) 32 | r = [] 33 | for i in l: 34 | r.append({'price': float(i['price']), 'amount': float(i['amount'])}) 35 | return r 36 | 37 | -------------------------------------------------------------------------------- /arbitrage/public_markets/_bitstar.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017, Philsong 2 | 3 | import urllib.request 4 | import urllib.error 5 | import urllib.parse 6 | import json 7 | from .market import Market 8 | 9 | class Bitstar(Market): 10 | def __init__(self, base_currency, market_currency, pair_code): 11 | super().__init__(base_currency, market_currency, pair_code) 12 | 13 | self.event = 'bitstar_depth' 14 | # self.subscribe_depth() 15 | 16 | def update_depth(self): 17 | url = 'https://www.bitstar.com/api/v1/market/depth/%s?size=50' % self.pair_code 18 | req = urllib.request.Request(url, headers={ 19 | "Content-Type": "application/x-www-form-urlencoded", 20 | "Accept": "*/*", 21 | "User-Agent": "curl/7.24.0 (x86_64-apple-darwin12.0)"}) 22 | res = urllib.request.urlopen(req) 23 | depth = json.loads(res.read().decode('utf8')) 24 | self.depth = self.format_depth(depth) 25 | 26 | 27 | -------------------------------------------------------------------------------- /arbitrage/public_markets/_bittrex.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017, Philsong 2 | 3 | import logging 4 | import requests 5 | from bittrex import bittrex 6 | from .market import Market 7 | 8 | 9 | # https://bittrex.com/api/v1.1/public/getorderbook?market=BTC-BCC&type=both 10 | # { 11 | # "success": true, 12 | # "message": "", 13 | # "result": { 14 | # "buy": [ 15 | # { 16 | # "Quantity": 96.83510777, 17 | # "Rate": 0.07303001 18 | # }, 19 | # { 20 | # } 21 | # ], 22 | # "sell": [ 23 | # { 24 | # "Quantity": 30.70354481, 25 | # "Rate": 0.07303002 26 | # }, 27 | # { 28 | # } 29 | # ] 30 | # } 31 | # } 32 | 33 | class Bittrex(Market): 34 | def __init__(self, base_currency, market_currency, pair_code): 35 | super().__init__(base_currency, market_currency, pair_code) 36 | 37 | self.client = bittrex.Bittrex('','') 38 | 39 | def update_depth(self): 40 | raw_depth = self.client.get_orderbook(self.pair_code, 'both') 41 | self.depth = self.format_depth(raw_depth) 42 | 43 | # override method 44 | def sort_and_format(self, l, reverse=False): 45 | l.sort(key=lambda x: float(x['Rate']), reverse=reverse) 46 | r = [] 47 | for i in l: 48 | r.append({'price': float(i['Rate']), 'amount': float(i['Quantity'])}) 49 | return r 50 | 51 | # override method 52 | def format_depth(self, depth): 53 | bids = self.sort_and_format(depth['result']['buy'], True) 54 | asks = self.sort_and_format(depth['result']['sell'], False) 55 | return {'asks': asks, 'bids': bids} 56 | -------------------------------------------------------------------------------- /arbitrage/public_markets/_huobi.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016, Philsong 2 | 3 | import urllib.request 4 | import urllib.error 5 | import urllib.parse 6 | import json 7 | from .market import Market 8 | 9 | class Huobi(Market): 10 | def __init__(self, base_currency, market_currency, pair_code): 11 | super().__init__(base_currency, market_currency, pair_code) 12 | 13 | 14 | self.event = 'huobi_depth' 15 | self.subscribe_depth() 16 | 17 | def update_depth(self): 18 | url = 'http://api.huobi.com/staticmarket/depth_%s_50.js' % self.pair_code 19 | req = urllib.request.Request(url, headers={ 20 | "Content-Type": "application/x-www-form-urlencoded", 21 | "Accept": "*/*", 22 | "User-Agent": "curl/7.24.0 (x86_64-apple-darwin12.0)"}) 23 | res = urllib.request.urlopen(req) 24 | depth = json.loads(res.read().decode('utf8')) 25 | self.depth = self.format_depth(depth) 26 | 27 | 28 | -------------------------------------------------------------------------------- /arbitrage/public_markets/_okcoin.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017, Philsong 2 | 3 | import urllib.request 4 | import urllib.error 5 | import urllib.parse 6 | import json 7 | import config 8 | from .market import Market 9 | 10 | class OKCoin(Market): 11 | def __init__(self, base_currency, market_currency, pair_code): 12 | super().__init__(base_currency, market_currency, pair_code) 13 | 14 | self.event = 'okcoin_depth' 15 | self.subscribe_depth() 16 | 17 | def update_depth(self): 18 | url = "https://www.okcoin.cn/api/v1/depth.do?symbol=" + self.pair_code 19 | req = urllib.request.Request(url, headers={ 20 | "Content-Type": "application/x-www-form-urlencoded", 21 | "Accept": "*/*", 22 | "User-Agent": "curl/7.24.0 (x86_64-apple-darwin12.0)"}) 23 | res = urllib.request.urlopen(req) 24 | depth = json.loads(res.read().decode('utf8')) 25 | self.depth = self.format_depth(depth) 26 | -------------------------------------------------------------------------------- /arbitrage/public_markets/bitfinex_bch_btc.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017, Philsong 2 | 3 | from ._bitfinex import Bitfinex 4 | 5 | 6 | # https://api.bitfinex.com/v1/symbols_details 7 | # { 8 | # "pair": "bchbtc", 9 | # "price_precision": 5, 10 | # "initial_margin": "30.0", 11 | # "minimum_margin": "15.0", 12 | # "maximum_order_size": "2000.0", 13 | # "minimum_order_size": "0.001", 14 | # "expiration": "NA" 15 | # }, 16 | 17 | class Bitfinex_BCH_BTC(Bitfinex): 18 | def __init__(self): 19 | super().__init__("BTC", "BCH", "bchbtc") 20 | 21 | if __name__ == "__main__": 22 | market = Bitfinex_BCH_BTC() 23 | print(market.get_ticker()) 24 | -------------------------------------------------------------------------------- /arbitrage/public_markets/bitfinex_btc_usd.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017, Philsong 2 | 3 | from ._bitfinex import Bitfinex 4 | 5 | class Bitfinex_BTC_USD(Bitfinex): 6 | def __init__(self): 7 | super().__init__("USD", "BTC", "btcusd") 8 | 9 | -------------------------------------------------------------------------------- /arbitrage/public_markets/bitstampusd.py: -------------------------------------------------------------------------------- 1 | import urllib.request 2 | import urllib.error 3 | import urllib.parse 4 | import json 5 | import sys 6 | from .market import Market 7 | 8 | 9 | class BitstampUSD(Market): 10 | def __init__(self): 11 | super(BitstampUSD, self).__init__("USD") 12 | self.update_rate = 20 13 | 14 | def update_depth(self): 15 | url = 'https://www.bitstamp.net/api/order_book/' 16 | req = urllib.request.Request(url, None, headers={ 17 | "Content-Type": "application/x-www-form-urlencoded", 18 | "Accept": "*/*", 19 | "User-Agent": "curl/7.24.0 (x86_64-apple-darwin12.0)"}) 20 | res = urllib.request.urlopen(req) 21 | depth = json.loads(res.read().decode('utf8')) 22 | self.depth = self.format_depth(depth) 23 | 24 | 25 | if __name__ == "__main__": 26 | market = BitstampUSD() 27 | print(market.get_ticker()) 28 | -------------------------------------------------------------------------------- /arbitrage/public_markets/bitstar_standardcny.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017, Philsong 2 | 3 | import urllib.request 4 | import urllib.error 5 | import urllib.parse 6 | import json 7 | from .market import Market 8 | import lib.bitstar_sdk as ApiClient 9 | 10 | class BS_StandardCNY(Market): 11 | def __init__(self): 12 | super().__init__('CNY') 13 | self.update_rate = 1 14 | self.client = ApiClient('', '') 15 | 16 | def update_depth(self): 17 | depth = {} 18 | try: 19 | publicinfo = self.client.publicinfo() 20 | print(publicinfo) 21 | depth['asks'] = [[publicinfo.standardprice, 1]] 22 | depth['bids'] = [[publicinfo.standardprice, 1]] 23 | except Exception as e: 24 | return 25 | 26 | self.depth = self.format_depth(depth) 27 | -------------------------------------------------------------------------------- /arbitrage/public_markets/bitstarcny.py: -------------------------------------------------------------------------------- 1 | from ._bitstar import Bitstar 2 | 3 | class BitstarCNY(Bitstar): 4 | def __init__(self): 5 | super().__init__("CNY", "swap-btc-cny") 6 | -------------------------------------------------------------------------------- /arbitrage/public_markets/bittrex_bch_btc.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017, Philsong 2 | 3 | from ._bittrex import Bittrex 4 | 5 | class Bittrex_BCH_BTC(Bittrex): 6 | def __init__(self): 7 | super().__init__("BTC", "BCH", "BTC-BCC") 8 | 9 | -------------------------------------------------------------------------------- /arbitrage/public_markets/brokercny.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016, Philsong 2 | 3 | import urllib.request 4 | import urllib.error 5 | import urllib.parse 6 | import json 7 | from .market import Market 8 | import lib.broker_api as exchange_api 9 | 10 | class BrokerCNY(Market): 11 | def __init__(self, base_currency, market_currency, pair_code): 12 | super().__init__(base_currency, market_currency, pair_code) 13 | 14 | exchange_api.init_broker() 15 | 16 | def update_depth(self): 17 | depth = {} 18 | try: 19 | ticker = exchange_api.exchange_get_ticker() 20 | depth['asks'] = [[ticker.ask, 30]] 21 | depth['bids'] = [[ticker.bid, 30]] 22 | except Exception as e: 23 | exchange_api.init_broker() 24 | return 25 | 26 | self.depth = self.format_depth(depth) 27 | -------------------------------------------------------------------------------- /arbitrage/public_markets/btceusd.py: -------------------------------------------------------------------------------- 1 | import urllib.request 2 | import urllib.error 3 | import urllib.parse 4 | import json 5 | from .market import Market 6 | 7 | 8 | class BtceUSD(Market): 9 | def __init__(self): 10 | super(BtceUSD, self).__init__("USD") 11 | self.update_rate = 60 12 | 13 | def update_depth(self): 14 | url = 'https://btc-e.com/api/2/btc_usd/depth' 15 | req = urllib.request.Request(url, None, headers={ 16 | "Content-Type": "application/x-www-form-urlencoded", 17 | "Accept": "*/*", 18 | "User-Agent": "curl/7.24.0 (x86_64-apple-darwin12.0)"}) 19 | res = urllib.request.urlopen(req) 20 | depth = json.loads(res.read().decode('utf8')) 21 | self.depth = self.format_depth(depth) 22 | 23 | def sort_and_format(self, l, reverse=False): 24 | l.sort(key=lambda x: float(x[0]), reverse=reverse) 25 | r = [] 26 | for i in l: 27 | r.append({'price': float(i[0]), 'amount': float(i[1])}) 28 | return r 29 | 30 | def format_depth(self, depth): 31 | bids = self.sort_and_format(depth['bids'], True) 32 | asks = self.sort_and_format(depth['asks'], False) 33 | return {'asks': asks, 'bids': bids} 34 | 35 | if __name__ == "__main__": 36 | market = BtceUSD() 37 | print(market.get_ticker()) 38 | -------------------------------------------------------------------------------- /arbitrage/public_markets/haobtccny.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016, Philsong 2 | 3 | import urllib.request 4 | import urllib.error 5 | import urllib.parse 6 | import json 7 | from .market import Market 8 | 9 | class HaobtcCNY(Market): 10 | def __init__(self): 11 | super().__init__('CNY') 12 | self.update_rate = 1 13 | self.event = 'haobtc_depth' 14 | self.subscribe_depth() 15 | raise 16 | 17 | def update_depth(self): 18 | url = 'https://api.bixin.com/exchange/api/v1/depth/?size=50' 19 | req = urllib.request.Request(url, headers={ 20 | "Content-Type": "application/x-www-form-urlencoded", 21 | "Accept": "*/*", 22 | "User-Agent": "curl/7.24.0 (x86_64-apple-darwin12.0)"}) 23 | res = urllib.request.urlopen(req) 24 | depth = json.loads(res.read().decode('utf8')) 25 | self.depth = self.format_depth(depth) 26 | -------------------------------------------------------------------------------- /arbitrage/public_markets/huobicny.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016, Philsong 2 | 3 | from ._huobi import Huobi 4 | 5 | class HuobiCNY(Huobi): 6 | def __init__(self): 7 | super().__init__("CNY", "btc") 8 | -------------------------------------------------------------------------------- /arbitrage/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 | import sys 8 | from fiatconverter import FiatConverter 9 | from utils import log_exception 10 | import traceback 11 | import config 12 | import threading 13 | 14 | class Market(object): 15 | def __init__(self, base_currency, market_currency, pair_code): 16 | self.name = self.__class__.__name__ 17 | self.base_currency = base_currency 18 | self.market_currency = market_currency 19 | self.pair_code = pair_code 20 | 21 | self.depth_updated = 0 22 | self.update_rate = 1 23 | self.fc = FiatConverter() 24 | self.fc.update() 25 | self.is_terminated = False 26 | self.request_timeout = 5 #5s 27 | 28 | def terminate(self): 29 | self.is_terminated = True 30 | 31 | def get_depth(self): 32 | timediff = time.time() - self.depth_updated 33 | # logging.warn('Market: %s order book1:(%s>%s)', self.name, timediff, self.depth_updated) 34 | if timediff > self.update_rate: 35 | # print('should update...') 36 | self.ask_update_depth() 37 | 38 | 39 | timediff = time.time() - self.depth_updated 40 | # logging.warn('Market: %s order book2:(%s>%s)', self.name, timediff, self.depth_updated) 41 | 42 | if timediff > config.market_expiration_time: 43 | # logging.warn('Market: %s order book is expired(%s>%s)', self.name, timediff, config.market_expiration_time) 44 | self.depth = {'asks': [{'price': 0, 'amount': 0}], 'bids': [ 45 | {'price': 0, 'amount': 0}]} 46 | return self.depth 47 | 48 | def convert_to_cny(self): 49 | if self.currency == "CNY": 50 | return 51 | for direction in ("asks", "bids"): 52 | for order in self.depth[direction]: 53 | order["price"] = self.fc.convert(order["price"], self.currency, "CNY") 54 | 55 | def subscribe_depth(self): 56 | if config.SUPPORT_ZMQ: 57 | t = threading.Thread(target = self.subscribe_zmq_depth) 58 | t.start() 59 | elif config.SUPPORT_WEBSOCKET: 60 | t = threading.Thread(target = self.subscribe_websocket_depth) 61 | t.start() 62 | else: 63 | pass 64 | 65 | def subscribe_zmq_depth(self): 66 | import lib.push as push 67 | 68 | push_s = push.Push(config.ZMQ_PORT) 69 | push_s.msg_server() 70 | 71 | def subscribe_websocket_depth(self): 72 | import json 73 | from socketIO_client import SocketIO 74 | 75 | def on_message(data): 76 | data = data.decode('utf8') 77 | if data[0] != '2': 78 | return 79 | 80 | data = json.loads(data[1:]) 81 | depth = data[1] 82 | 83 | logging.debug("depth coming: %s", depth['market']) 84 | self.depth_updated = int(depth['timestamp']/1000) 85 | self.depth = self.format_depth(depth) 86 | 87 | def on_connect(): 88 | logging.info('[Connected]') 89 | 90 | socketIO.emit('land', {'app': 'haobtcnotify', 'events':[self.event]}); 91 | 92 | with SocketIO(config.WEBSOCKET_HOST, port=config.WEBSOCKET_PORT) as socketIO: 93 | 94 | socketIO.on('connect', on_connect) 95 | socketIO.on('message', on_message) 96 | 97 | socketIO.wait() 98 | 99 | def ask_update_depth(self): 100 | try: 101 | self.update_depth() 102 | # self.convert_to_usd() 103 | self.depth_updated = time.time() 104 | except Exception as e: 105 | logging.error("Can't update market: %s - %s" % (self.name, str(e))) 106 | log_exception(logging.DEBUG) 107 | # traceback.print_exc() 108 | 109 | def get_ticker(self): 110 | depth = self.get_depth() 111 | res = {'ask': 0, 'bid': 0} 112 | if len(depth['asks']) > 0 and len(depth["bids"]) > 0: 113 | res = {'ask': depth['asks'][0], 114 | 'bid': depth['bids'][0]} 115 | return res 116 | 117 | def sort_and_format(self, l, reverse=False): 118 | l.sort(key=lambda x: float(x[0]), reverse=reverse) 119 | r = [] 120 | for i in l: 121 | r.append({'price': float(i[0]), 'amount': float(i[1])}) 122 | return r 123 | 124 | def format_depth(self, depth): 125 | bids = self.sort_and_format(depth['bids'], True) 126 | asks = self.sort_and_format(depth['asks'], False) 127 | return {'asks': asks, 'bids': bids} 128 | 129 | ## Abstract methods 130 | def update_depth(self): 131 | pass 132 | 133 | def buy(self, price, amount): 134 | pass 135 | 136 | def sell(self, price, amount): 137 | pass 138 | -------------------------------------------------------------------------------- /arbitrage/public_markets/okcoincny.py: -------------------------------------------------------------------------------- 1 | from ._okcoin import OKCoin 2 | 3 | class OKCoinCNY(OKCoin): 4 | def __init__(self): 5 | super().__init__("CNY", "btc_cny") 6 | -------------------------------------------------------------------------------- /arbitrage/test/arbitrage_speed_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('../') 3 | import json 4 | import arbitrage 5 | import time 6 | from observers import observer 7 | 8 | 9 | class TestObserver(observer.Observer): 10 | def opportunity(self, profit, volume, buyprice, kask, sellprice, kbid, 11 | perc, weighted_buyprice, weighted_sellprice): 12 | print("Time: %.3f" % profit) 13 | 14 | def main(): 15 | arbitrer = arbitrage.Arbitrer() 16 | depths = arbitrer.depths = json.load(open("speed-test.json")) 17 | start_time = time.time() 18 | testobs = TestObserver() 19 | arbitrer.observers = [testobs] 20 | arbitrer.arbitrage_opportunity("BitstampUSD", depths["BitstampUSD"]["asks"][0], 21 | "MtGoxEUR", depths["MtGoxEUR"]["asks"][0]) 22 | # FIXME: add asserts 23 | elapsed = time.time() - start_time 24 | print("Time: %.3f" % elapsed) 25 | 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /arbitrage/test/arbitrage_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('../') 3 | import unittest 4 | 5 | import arbitrage 6 | 7 | depths1 = { 8 | 'PaymiumEUR': 9 | {'asks': [{'amount': 4, 'price': 32.8}, 10 | {'amount': 8, 'price': 32.9}, 11 | {'amount': 2, 'price': 33.0}, 12 | {'amount': 3, 'price': 33.6}], 13 | 'bids': [{'amount': 2, 'price': 31.8}, 14 | {'amount': 4, 'price': 31.6}, 15 | {'amount': 6, 'price': 31.4}, 16 | {'amount': 2, 'price': 30}]}, 17 | 'MtGoxEUR': 18 | {'asks': [{'amount': 1, 'price': 34.2}, 19 | {'amount': 2, 'price': 34.3}, 20 | {'amount': 3, 'price': 34.5}, 21 | {'amount': 3, 'price': 35.0}], 22 | 'bids': [{'amount': 2, 'price': 33.2}, 23 | {'amount': 3, 'price': 33.1}, 24 | {'amount': 5, 'price': 32.6}, 25 | {'amount': 10, 'price': 32.3}]}} 26 | 27 | depths2 = { 28 | 'PaymiumEUR': 29 | {'asks': [{'amount': 4, 'price': 32.8}, 30 | {'amount': 8, 'price': 32.9}, 31 | {'amount': 2, 'price': 33.0}, 32 | {'amount': 3, 'price': 33.6}]}, 33 | 'MtGoxEUR': 34 | {'bids': [{'amount': 2, 'price': 33.2}, 35 | {'amount': 3, 'price': 33.1}, 36 | {'amount': 5, 'price': 32.6}, 37 | {'amount': 10, 'price': 32.3}]}} 38 | 39 | depths3 = { 40 | 'PaymiumEUR': 41 | {'asks': [{'amount': 1, 'price': 34.2}, 42 | {'amount': 2, 'price': 34.3}, 43 | {'amount': 3, 'price': 34.5}, 44 | {'amount': 3, 'price': 35.0}]}, 45 | 'MtGoxEUR': 46 | {'bids': [{'amount': 2, 'price': 33.2}, 47 | {'amount': 3, 'price': 33.1}, 48 | {'amount': 5, 'price': 32.6}, 49 | {'amount': 10, 'price': 32.3}]}} 50 | 51 | 52 | class TestArbitrage(unittest.TestCase): 53 | def setUp(self): 54 | self.arbitrer = arbitrage.Arbitrer() 55 | 56 | def test_getprofit1(self): 57 | self.arbitrer.depths = depths2 58 | profit, vol, wb, ws = self.arbitrer.get_profit_for( 59 | 0, 0, 'PaymiumEUR', 'MtGoxEUR') 60 | assert(80 == int(profit * 100)) 61 | assert(vol == 2) 62 | 63 | def test_getprofit2(self): 64 | self.arbitrer.depths = depths2 65 | profit, vol, wb, ws = self.arbitrer.get_profit_for( 66 | 2, 1, 'PaymiumEUR', 'MtGoxEUR') 67 | assert(159 == int(profit * 100)) 68 | assert(vol == 5) 69 | 70 | def test_getprofit3(self): 71 | self.arbitrer.depths = depths3 72 | profit, vol, wb, ws = self.arbitrer.get_profit_for( 73 | 2, 1, 'PaymiumEUR', 'MtGoxEUR') 74 | assert(profit == 0) 75 | assert(vol == 0) 76 | 77 | if __name__ == '__main__': 78 | unittest.main() 79 | -------------------------------------------------------------------------------- /arbitrage/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 = (os.path.basename(i[0]), i[1], i[2]) 10 | line = (i[0], i[1], i[2]) 11 | logging.log(level, 'File "%s", line %d, in %s' % line) 12 | logging.log(level, '\t%s' % i[3]) 13 | -------------------------------------------------------------------------------- /docs/add-new-exchange.md: -------------------------------------------------------------------------------- 1 | # How To Add A New Exchange 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | zmq 3 | thriftpy 4 | -e git+https://github.com/philsong/bitfinex.git#egg=bitfinex 5 | -e git+https://github.com/philsong/python-bittrex.git#egg=python-bittrex -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | import sys 5 | 6 | 7 | if sys.version_info < (3,): 8 | print("crypto-arbitrage requires Python version >= 3.0") 9 | sys.exit(1) 10 | 11 | setup(name='crypto-arbitrage', 12 | packages = ["arbitrage"], 13 | version='0.3', 14 | description='crypto asset arbitrage opportunity watcher, market maker, hedge and arbitrage', 15 | author='Phil Song', 16 | author_email='songbohr@gmail.com', 17 | url='https://github.com/philsong/crypto-arbitrage', 18 | arbitrage=['bin/crypto-arbitrage'], 19 | test_suite='nose.collector', 20 | tests_require=['nose'], 21 | ) 22 | -------------------------------------------------------------------------------- /tools/autopep8-project.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | find . -name "*.py" -or -name "config.py-example" | xargs autopep8 -i 3 | --------------------------------------------------------------------------------