├── .gitignore ├── Arbitrage.py ├── Broker.py ├── Exchange.py ├── GDAX ├── .gitignore ├── AuthenticatedClient.py ├── PublicClient.py ├── WebsocketClient.py └── __init__.py ├── Order.py ├── Profit.py ├── README.md ├── __init__.py ├── btce.py ├── btceapi ├── __init__.py ├── common.py ├── keyhandler.py ├── public.py ├── scraping.py └── trade.py ├── build_brokers.py ├── config.py ├── config_key.py ├── poloniex.py ├── poloniex └── __init__.py └── run.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.pyc 3 | btce_keys.txt 4 | -------------------------------------------------------------------------------- /Arbitrage.py: -------------------------------------------------------------------------------- 1 | from Profit import Profit 2 | # Runs the arbitrage strategy 3 | 4 | # Make it Arbitrage(Bot) , write a bot baseclass 5 | class Arbitrage(object): 6 | def __init__(self, config, brokers): 7 | super(Arbitrage, self).__init__() 8 | self.config = config 9 | self.brokers = brokers 10 | 11 | if self.config.TRADE_MODE == 'paper': 12 | for broker in self.brokers: 13 | broker.initialize_balance() 14 | 15 | def run_trade(self, pair): 16 | for broker in self.brokers: 17 | broker.get_depth(pair) 18 | base, alt = pair 19 | print "Searching for arbitrage in the {}_{} market".format(base, alt) 20 | profit = Profit(self.brokers, pair) 21 | # Checks for existence of profitable trades 22 | profit.check_spread() 23 | # TODO: Have check spread return data, and print handled by a seperate function 24 | # TODO: Implement actual balance calculations even for Paper trading 25 | # for pair in self.config.PAIRS: 26 | # for broker in self.brokers: 27 | # # Update balances for each currency in the broker object 28 | # broker.update_all_balances() -------------------------------------------------------------------------------- /Broker.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import config 3 | 4 | class Broker(object): 5 | def __init__(self, mode, exchange): 6 | super(Broker, self).__init__() 7 | self.mode = mode 8 | self.exchange = exchange 9 | 10 | self.balances = {} 11 | self.orders = [] # list of orders in the book 12 | self.depth = {} # "base" : ([buy orders], [sell orders]) 13 | 14 | def get_pairstr(self, pair): 15 | base, alt = pair 16 | return base.upper() + "_" + alt.upper() 17 | 18 | def get_depth(self, pair): 19 | base, alt = pair 20 | pairstr = self.get_pairstr(pair) 21 | self.depth[pairstr] = self.exchange.get_depth(base, alt) 22 | 23 | # Sort the bids in descending order and asks in ascending order 24 | self.depth[pairstr]["bids"].sort(key=lambda x : x.price, reverse=True) 25 | self.depth[pairstr]["asks"].sort(key=lambda x : x.price, reverse=False) 26 | 27 | def get_highest_bid(self, pair): 28 | # self.get_depth(pair) 29 | pairstr = self.get_pairstr(pair) 30 | return self.depth[pairstr]["bids"][0] 31 | 32 | def get_lowest_ask(self, pair): 33 | # self.get_depth(pair) 34 | pairstr = self.get_pairstr(pair) 35 | return self.depth[pairstr]["asks"][0] 36 | 37 | def update_all_balances(self): 38 | if self.mode == 'paper': 39 | pass 40 | elif self.mode == 'real': 41 | self.balances = self.exchange.get_all_balances() 42 | else: 43 | logging.error('This mode is unsupported: ' + self.mode) 44 | 45 | # Only initialize balance when paper trading 46 | def initialize_balance(self): 47 | self.balances = config.INITIAL_BALANCE -------------------------------------------------------------------------------- /Exchange.py: -------------------------------------------------------------------------------- 1 | import config 2 | 3 | class Exchange(object): 4 | 5 | def __init__(self): 6 | super(Exchange, self).__init__() 7 | self.name = None 8 | self.trading_fee = None 9 | self.ok =True 10 | self.tradeable_pairs = self.get_tradeable_pairs() 11 | 12 | def get_validated_pair(self, pair): 13 | # Checks for existence of supported pain in exchange 14 | # If valid returns pair, swapped(bool) 15 | base, alt = pair 16 | if pair in self.tradeable_pairs: 17 | return (pair, False) 18 | elif (alt, base) in self.tradeable_pairs: 19 | return ((alt, base), True) 20 | else: 21 | # pair is not traded 22 | return None 23 | 24 | def get_min_vol(self, pair, depth): 25 | base, alt = pair 26 | test = self.get_validated_pair(pair) 27 | if test is not None: 28 | true_pair, swapped = test 29 | if not swapped: 30 | true_base, true_alt = true_pair 31 | return config.MINIMUM_PROFIT_VOLUME[true_base.upper()] 32 | # TODO: Implement the alternate function 33 | else: 34 | return get_converted_alt_volume() 35 | 36 | def get_tradeable_pairs(self): 37 | pass -------------------------------------------------------------------------------- /GDAX/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | FixClient.py -------------------------------------------------------------------------------- /GDAX/AuthenticatedClient.py: -------------------------------------------------------------------------------- 1 | # 2 | # GDAX/AuthenticatedClient.py 3 | # Daniel Paquin 4 | # 5 | # For authenticated requests to the GDAX exchange 6 | 7 | import hmac, hashlib, time, requests, base64 8 | from requests.auth import AuthBase 9 | from GDAX.PublicClient import PublicClient 10 | 11 | class AuthenticatedClient(PublicClient): 12 | def __init__(self, key, b64secret, passphrase, api_url="https://api.gdax.com", product_id="BTC-USD"): 13 | self.url = api_url 14 | self.productId = product_id 15 | self.auth = GdaxAuth(key, b64secret, passphrase) 16 | 17 | def getAccount(self, accountId): 18 | r = requests.get(self.url + '/accounts/' + accountId, auth=self.auth) 19 | return r.json() 20 | 21 | def getAccounts(self): 22 | return self.getAccount('') 23 | 24 | def getAccountHistory(self, accountId): 25 | list = [] 26 | r = requests.get(self.url + '/accounts/%s/ledger' %accountId, auth=self.auth) 27 | list.append(r.json()) 28 | if "cb-after" in r.headers: 29 | self.historyPagination(accountId, list, r.headers["cb-after"]) 30 | return list 31 | 32 | def historyPagination(self, accountId, list, after): 33 | r = requests.get(self.url + '/accounts/%s/ledger?after=%s' %(accountId, str(after)), auth=self.auth) 34 | if r.json(): 35 | list.append(r.json()) 36 | if "cb-after" in r.headers: 37 | self.historyPagination(accountId, list, r.headers["cb-after"]) 38 | return list 39 | 40 | def getAccountHolds(self, accountId): 41 | list = [] 42 | r = requests.get(self.url + '/accounts/%s/holds' %accountId, auth=self.auth) 43 | list.append(r.json()) 44 | if "cb-after" in r.headers: 45 | self.holdsPagination(accountId, list, r.headers["cb-after"]) 46 | return list 47 | 48 | def holdsPagination(self, accountId, list, after): 49 | r = requests.get(self.url + '/accounts/%s/holds?after=%s' %(accountId, str(after)), auth=self.auth) 50 | if r.json(): 51 | list.append(r.json()) 52 | if "cb-after" in r.headers: 53 | self.holdsPagination(accountId, list, r.headers["cb-after"]) 54 | return list 55 | 56 | def buy(self, buyParams): 57 | buyParams["side"] = "buy" 58 | if not buyParams["product_id"]: 59 | buyParams["product_id"] = self.productId 60 | r = requests.post(self.url + '/orders', json=buyParams, auth=self.auth) 61 | return r.json() 62 | 63 | def sell(self, sellParams): 64 | sellParams["side"] = "sell" 65 | r = requests.post(self.url + '/orders', json=sellParams, auth=self.auth) 66 | return r.json() 67 | 68 | def cancelOrder(self, orderId): 69 | r = requests.delete(self.url + '/orders/' + orderId, auth=self.auth) 70 | return r.json() 71 | 72 | def getOrder(self, orderId): 73 | r = requests.get(self.url + '/orders/' + orderId, auth=self.auth) 74 | return r.json() 75 | 76 | def getOrders(self): 77 | list = [] 78 | r = requests.get(self.url + '/orders/', auth=self.auth) 79 | list.append(r.json()) 80 | if 'cb-after' in r.headers: 81 | self.paginateOrders(list, r.headers['cb-after']) 82 | return list 83 | 84 | def paginateOrders(self, list, after): 85 | r = requests.get(self.url + '/orders?after=%s' %str(after)) 86 | if r.json(): 87 | list.append(r.json()) 88 | if 'cb-after' in r.headers: 89 | self.paginateOrders(list, r.headers['cb-after']) 90 | return list 91 | 92 | def getFills(self, orderId='', productId='', before='', after='', limit=''): 93 | list = [] 94 | url = self.url + '/fills?' 95 | if orderId: url += "order_id=%s&" %str(orderId) 96 | if productId: url += "product_id=%s&" %(productId or self.productId) 97 | if before: url += "before=%s&" %str(before) 98 | if after: url += "after=%s&" %str(after) 99 | if limit: url += "limit=%s&" %str(limit) 100 | r = requests.get(url, auth=self.auth) 101 | list.append(r.json()) 102 | if 'cb-after' in r.headers and limit is not len(r.json()): 103 | return self.paginateFills(list, r.headers['cb-after'], orderId=orderId, productId=productId) 104 | return list 105 | 106 | def paginateFills(self, list, after, orderId='', productId=''): 107 | url = self.url + '/fills?after=%s&' % str(after) 108 | if orderId: url += "order_id=%s&" % str(orderId) 109 | if productId: url += "product_id=%s&" % (productId or self.productId) 110 | r = requests.get(url, auth=self.auth) 111 | if r.json(): 112 | list.append(r.json()) 113 | if 'cb-after' in r.headers: 114 | return self.paginateFills(list, r.headers['cb-after'], orderId=orderId, productId=productId) 115 | return list 116 | 117 | def deposit(self, amount="", accountId=""): 118 | payload = { 119 | "type": "deposit", 120 | "amount": amount, 121 | "accountId": accountId 122 | } 123 | r = requests.post(self.url + "/transfers", json=payload, auth=self.auth) 124 | return r.json() 125 | 126 | def withdraw(self, amount="", accountId=""): 127 | payload = { 128 | "type": "withdraw", 129 | "amount": amount, 130 | "accountId": accountId 131 | } 132 | r = requests.post(self.url + "/transfers", json=payload, auth=self.auth) 133 | return r.json() 134 | 135 | class GdaxAuth(AuthBase): 136 | # Provided by Coinbase: https://docs.gdax.com/#signing-a-message 137 | def __init__(self, api_key, secret_key, passphrase): 138 | self.api_key = api_key 139 | self.secret_key = secret_key 140 | self.passphrase = passphrase 141 | 142 | def __call__(self, request): 143 | timestamp = str(time.time()) 144 | message = timestamp + request.method + request.path_url + (request.body or '') 145 | message = message.encode('ascii') 146 | hmac_key = base64.b64decode(self.secret_key) 147 | signature = hmac.new(hmac_key, message, hashlib.sha256) 148 | signature_b64 = base64.b64encode(signature.digest()) 149 | request.headers.update({ 150 | 'CB-ACCESS-SIGN': signature_b64, 151 | 'CB-ACCESS-TIMESTAMP': timestamp, 152 | 'CB-ACCESS-KEY': self.api_key, 153 | 'CB-ACCESS-PASSPHRASE': self.passphrase, 154 | }) 155 | return request 156 | -------------------------------------------------------------------------------- /GDAX/PublicClient.py: -------------------------------------------------------------------------------- 1 | # 2 | # GDAX/PublicClient.py 3 | # Daniel Paquin 4 | # 5 | # For public requests to the GDAX exchange 6 | 7 | import requests 8 | 9 | class PublicClient(): 10 | def __init__(self, api_url="https://api.gdax.com", product_id="BTC-USD"): 11 | self.url = api_url 12 | self.productId = product_id 13 | 14 | def getProducts(self): 15 | response = requests.get(self.url + '/products') 16 | return response.json() 17 | 18 | def getProductOrderBook(self, level=2, product=''): 19 | response = requests.get(self.url + '/products/%s/book?level=%s' % (product or self.productId, str(level))) 20 | return response.json() 21 | 22 | def getProductTicker(self, product=''): 23 | response = requests.get(self.url + '/products/%s/ticker' % (product or self.productId)) 24 | return response.json() 25 | 26 | def getProductTrades(self, product=''): 27 | response = requests.get(self.url + '/products/%s/trades' % (product or self.productId)) 28 | return response.json() 29 | 30 | def getProductHistoricRates(self, product='', start='', end='', granularity=''): 31 | payload = { 32 | "start" : start, 33 | "end" : end, 34 | "granularity" : granularity 35 | } 36 | response = requests.get(self.url + '/products/%s/candles' % (product or self.productId), params=payload) 37 | return response.json() 38 | 39 | def getProduct24HrStats(self, product=''): 40 | response = requests.get(self.url + '/products/%s/stats' % (product or self.productId)) 41 | return response.json() 42 | 43 | def getCurrencies(self): 44 | response = requests.get(self.url + '/currencies') 45 | return response.json() 46 | 47 | def getTime(self): 48 | response = requests.get(self.url + '/time') 49 | return response.json() -------------------------------------------------------------------------------- /GDAX/WebsocketClient.py: -------------------------------------------------------------------------------- 1 | # 2 | # GDAX/WebsocketClient.py 3 | # Daniel Paquin 4 | # 5 | # Template object to receive messages from the GDAX Websocket Feed 6 | 7 | import json 8 | from threading import Thread 9 | import time 10 | from websocket import create_connection 11 | 12 | class WebsocketClient(): 13 | def __init__(self, ws_url="wss://ws-feed.gdax.com", product_id="BTC-USD"): 14 | self.stop = False 15 | self.url = ws_url 16 | self.product_id = product_id 17 | self.thread = Thread(target=self.setup) 18 | self.thread.start() 19 | 20 | def setup(self): 21 | self.open() 22 | self.ws = create_connection(self.url) 23 | subParams = json.dumps({"type": "subscribe", "product_id": self.product_id}) 24 | self.ws.send(subParams) 25 | self.listen() 26 | 27 | def open(self): 28 | print("-- Subscribed! --") 29 | 30 | def listen(self): 31 | while not self.stop: 32 | try: 33 | msg = json.loads(self.ws.recv()) 34 | except Exception as e: 35 | #print e 36 | break 37 | else: 38 | self.message(msg) 39 | 40 | def message(self, msg): 41 | print(msg) 42 | 43 | def close(self): 44 | self.ws.close() 45 | self.closed() 46 | 47 | def closed(self): 48 | print("Socket Closed") 49 | 50 | if __name__ == "__main__": 51 | newWS = WebsocketClient() # Runs in a separate thread 52 | try: 53 | while True: 54 | time.sleep(0.1) 55 | except KeyboardInterrupt: 56 | newWS.stop = True 57 | newWS.thread.join() 58 | newWS.close() 59 | -------------------------------------------------------------------------------- /GDAX/__init__.py: -------------------------------------------------------------------------------- 1 | from GDAX.AuthenticatedClient import AuthenticatedClient 2 | from GDAX.PublicClient import PublicClient 3 | from GDAX.WebsocketClient import WebsocketClient 4 | -------------------------------------------------------------------------------- /Order.py: -------------------------------------------------------------------------------- 1 | class Order(object): 2 | def __init__(self, price, volume): 3 | self.price = price 4 | self.volume = volume 5 | 6 | def __repr__(self): 7 | return "Order of price : {} , volume : {}".format(self.price, self.volume) -------------------------------------------------------------------------------- /Profit.py: -------------------------------------------------------------------------------- 1 | import config 2 | import logging 3 | 4 | class Profit(object): 5 | def __init__(self, brokers, pair): 6 | self.brokers = brokers 7 | self.pair = pair # (bidderexchange, askerexchange) 8 | self.spread = {} # maintains the profit spread matrix for a pair between all exchanges 9 | self.prices = {} # maintains high bids and low asks for each exchange 10 | 11 | self.build_profit_spread() 12 | 13 | def build_profit_spread(self): 14 | self.spread = { b.exchange.name : { a.exchange.name : 0 for a in self.brokers } for b in self.brokers} # { bidder : {asker, spread} } 15 | self.prices = { b.exchange.name : { "bid" : b.get_highest_bid(self.pair), 16 | "ask" : b.get_lowest_ask(self.pair)} for b in self.brokers} 17 | 18 | for bidder in self.brokers: 19 | for asker in self.brokers: 20 | bid_price = self.prices[bidder.exchange.name]["bid"] 21 | ask_price = self.prices[asker.exchange.name]["ask"] 22 | 23 | if bid_price is None or ask_price is None: 24 | self.spread[bidder.exchange.name][asker.exchange.name] = None 25 | else: 26 | self.spread[bidder.exchange.name][asker.exchange.name] = self.calc_profit_spread(bid_price.price, bidder.exchange.bid_fee, ask_price.price, asker.exchange.ask_fee) 27 | 28 | # self.print_spread() 29 | 30 | def calc_profit_spread(self, bid_price, bidder_fee, ask_price, asker_fee): 31 | # This formula is representative of the spread and doesn't take order volume into account 32 | # Basically if high_bid = 510 and low_ask = 500 33 | # With the high_bid you will be able to to get the amount of 510 if you sell .998 of alt because of the fee 34 | # So we need to buy exactly .998 of alt from base of which .998 of this is only effective 35 | # Basically whatever we spend to buy alt using base we only get .998 for 1 36 | # Since 500 * .002 = 1 (fee) 37 | # so we have 500 - 1 = 499 base to spend 38 | # We get 499 / 500 of alt = .998 alt 39 | # Now this is the true price per 1 alt 40 | # Now we have .998 of alt to sell at a rate of 510 with a fee 41 | # so (.998 * 510) - (.998 * .002 * 510) = Amt of money got for that amt of alt - fee charged on the received money 42 | # therefore = .998 * .998 * 510 = 507.9624 43 | # There for profit spread in this case is 507.9624 - 500 44 | return ((1.0 - bidder_fee) * (1.0 - asker_fee) * bid_price) - ask_price 45 | 46 | def print_spread(self): 47 | for key, asker_spread in self.spread.iteritems(): 48 | print key 49 | for k, val in asker_spread.iteritems(): 50 | print k 51 | print val 52 | 53 | def calc_real_profit(self, bidder, asker, bids, asks, pair): 54 | # Since we are only trying to fulfill trades with the first order from the book 55 | # We check if that order satisfies minimum volume 56 | # It's probably safe to assume orders placed are valid :/ 57 | min_bid_vol = bidder.exchange.get_min_vol(pair, bidder.depth) 58 | min_ask_vol = asker.exchange.get_min_vol(pair, asker.depth) 59 | 60 | asker_factor = 1.0 - asker.exchange.ask_fee 61 | bidder_factor = 1.0 - bidder.exchange.bid_fee 62 | base, alt = pair 63 | 64 | # We want to fulfill the order using the best prices so we trade with the least volume on offer 65 | # vol_reqd is the volume of alt we need 66 | vol_reqd = min(asks[0].volume * asker_factor, bids[0].volume) 67 | 68 | # TODO: Check with balance of each currency and volume to find the optimized volume_required 69 | 70 | if min_bid_vol < vol_reqd and min_ask_vol < vol_reqd: 71 | # Check if we have enough balance in each exchange 72 | if (asker.balances[base] >= (vol_reqd/asker_factor) * asks[0].volume) and (bidder.balances[alt] >= vol_reqd): 73 | spent_btc_at_asker = (vol_reqd/asker_factor) * asks[0].price 74 | gained_btc_at_bidder = vol_reqd * bids[0].price * bidder_factor 75 | profit = gained_btc_at_bidder - spent_btc_at_asker 76 | 77 | # TODO: Add a check to make sure we don't move too much currency relative to the profit 78 | 79 | if profit > config.MINIMUM_PROFIT_VOLUME["BTC"]: 80 | print "SUCCESS -> Arbitrage oppurtunity discovered" 81 | print "Buy {} of {} of which lowest ask is {} for {} of {}".format(vol_reqd, alt, asker.exchange.name ,asks[0].price, base) 82 | print "Sell {} of {} of which highest bid is {} for {} of {}".format(vol_reqd, alt, bidder.exchange.name, bids[0].price, base) 83 | print "PROFIT: {} of {}".format(profit, base) 84 | else: 85 | logging.error("The balance in the exchanges was not enough to make a trade") 86 | else: 87 | logging.error("Not enough volume for the exchange") 88 | 89 | def check_spread(self): 90 | for bidder in self.brokers: 91 | for asker in self.brokers: 92 | profit_spread = self.spread[bidder.exchange.name][asker.exchange.name] 93 | base, alt = self.pair 94 | # Check if spread's greater than minimum profit 95 | if profit_spread is not None and profit_spread >= config.MINIMUM_PROFIT_VOLUME[base]: 96 | # Check depth 97 | 98 | bids = bidder.depth[base + "_" + alt]["bids"] 99 | asks = asker.depth[base + "_" + alt]["asks"] 100 | self.calc_real_profit(bidder, asker, bids, asks, self.pair) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitcoin Arbitrage Bot 2 | 3 | *Within the inefficiencies of the markets exist opportunities for those who wish to find them.* 4 | 5 | This is an automated trading bot that finds arbitrage opportunities between various cryptocurrency exchanges 6 | from around the world. 7 | 8 | The program will run pairwise arbitrage for *LTC-BTC* and *ETH-BTC*. 9 | 10 | ## Run: 11 | * Run `run.py` 12 | 13 | 14 | 15 | 16 | 17 | 18 | ## Support: 19 | 20 | It currently works for: 21 | * [BTC-E](btc-e.com) 22 | * [Poloniex](poloniex.com) (Work in Progress) 23 | * [GDAX](gdax.com) (Work in Progress) 24 | * [Bitfinex](bitfinex.com) (Work in Progress) 25 | * [BTCC](btcc.com) (Work in Progress) 26 | 27 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suhithr/bitcoin-arbitrage-bot/18fad6a7e7e32e4752eb15b714c5cbe672941ab6/__init__.py -------------------------------------------------------------------------------- /btce.py: -------------------------------------------------------------------------------- 1 | from Exchange import Exchange 2 | from Order import Order 3 | import btceapi 4 | import os 5 | import logging 6 | 7 | class BTCE(Exchange): 8 | def __init__(self, keypath): 9 | keyfile = os.path.abspath(keypath) 10 | self.keyhandler = btceapi.KeyHandler(keyfile) 11 | key = self.keyhandler.getKeys()[0] 12 | self.conn = btceapi.BTCEConnection() 13 | self.api = btceapi.TradeAPI(key, self.keyhandler) 14 | 15 | self.name = 'BTCE' 16 | self.trading_fee = 0.002 # The fee is 0.2% for all pairs, maker and taker 17 | self.bid_fee = self.trading_fee 18 | self.ask_fee = self.trading_fee 19 | self.tradeable_pairs = self.get_tradeable_pairs() 20 | self.minimum_amount = {} 21 | self.decimal_places = {} 22 | self.get_info() 23 | 24 | def get_tradeable_pairs(self): 25 | tradeable_pairs = [] 26 | for pair in btceapi.all_pairs: 27 | a, b = pair.split("_") 28 | tradeable_pairs.append((a.upper(), b.upper())) 29 | return tradeable_pairs 30 | 31 | def get_depth(self, base, alt): 32 | order_book = {'bids': [], 'asks': []} 33 | pair, swapped = self.get_validated_pair((base, alt)) 34 | if pair is None: 35 | return 36 | 37 | pairstr = pair[0].lower() + "_" + pair[1].lower() 38 | if swapped: 39 | bids, asks = btceapi.getDepth(pairstr) 40 | else: 41 | asks, bids = btceapi.getDepth(pairstr) 42 | 43 | order_book['bids'] = [Order(float(b[0]), float(b[1])) for b in bids] 44 | order_book['asks'] = [Order(float(a[0]), float(a[1])) for a in asks] 45 | return order_book 46 | 47 | def get_min_vol(self, pair, depth): 48 | base, alt = pair 49 | slug = base + "_" + alt 50 | test = self.get_validated_pair(pair) 51 | if test is not None: 52 | true_pair, swapped = test 53 | pairstr = "{}_{}".format(true_pair[0].lower(), true_pair[1].lower()) 54 | return self.minimum_amount[pairstr] 55 | # Does swapped matter? 56 | 57 | def get_balance(self, currency): 58 | data = self.api.getInfo(connection = self.conn) 59 | return getattr(data, 'balance_' + currency.lower()) 60 | 61 | def get_all_balances(self): 62 | balances = self.api.getBalances(self.conn) 63 | return balances 64 | 65 | # The wrapper used is outdated, so I'm writing my own function to add support for the Public Info function 66 | # the method info is https://btc-e.com/api/3/docs#info 67 | def get_info(self): 68 | connection = btceapi.common.BTCEConnection() 69 | info = connection.makeJSONRequest("/api/3/info")["pairs"] 70 | self.minimum_amount.update({pair : info[pair]["min_amount"] for pair in btceapi.all_pairs}) 71 | self.decimal_places.update({pair : info[pair]["decimal_places"] for pair in btceapi.all_pairs}) 72 | -------------------------------------------------------------------------------- /btceapi/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2015 Alan McIntyre 2 | 3 | from public import getDepth, getTicker, getTradeFee, getTradeHistory 4 | from trade import TradeAPI 5 | from scraping import scrapeMainPage 6 | from keyhandler import AbstractKeyHandler, KeyHandler 7 | from common import all_currencies, all_pairs, max_digits, min_orders, \ 8 | formatCurrency, formatCurrencyDigits, \ 9 | truncateAmount, truncateAmountDigits, \ 10 | validatePair, validateOrder, \ 11 | BTCEConnection 12 | 13 | __version__ = "0.3.1" 14 | -------------------------------------------------------------------------------- /btceapi/common.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2015 Alan McIntyre 2 | 3 | import decimal 4 | import httplib 5 | import json 6 | import re 7 | import os 8 | 9 | 10 | class InvalidTradePairException(Exception): 11 | """ Exception raised when an invalid pair is passed. """ 12 | pass 13 | 14 | 15 | class InvalidTradeTypeException(Exception): 16 | """ Exception raise when invalid trade type is passed. """ 17 | pass 18 | 19 | 20 | class InvalidTradeAmountException(Exception): 21 | """ Exception raised if trade amount is too much or too little. """ 22 | pass 23 | 24 | 25 | class APIResponseError(Exception): 26 | """ Exception raise if the API replies with an HTTP code 27 | not in the 2xx range. """ 28 | pass 29 | 30 | 31 | decimal.getcontext().rounding = decimal.ROUND_DOWN 32 | exps = [decimal.Decimal("1e-%d" % i) for i in range(16)] 33 | 34 | btce_domain = "btc-e.com" 35 | 36 | all_currencies = ("btc", "ltc", "nmc", "nvc", "ppc", "dsh", "eth", "usd", "rur", "eur") 37 | all_pairs = ("btc_usd", "btc_rur", "btc_eur", 38 | "ltc_btc", "ltc_usd", "ltc_rur", "ltc_eur", 39 | "nmc_btc", "nmc_usd", 40 | "nvc_btc", "nvc_usd", 41 | "usd_rur", "eur_usd", "eur_rur", 42 | "ppc_btc", "ppc_usd", 43 | "dsh_btc", 44 | "eth_btc", "eth_usd", "eth_ltc") 45 | 46 | max_digits = {"btc_usd": 3, 47 | "btc_rur": 5, 48 | "btc_eur": 5, 49 | "ltc_btc": 5, 50 | "ltc_usd": 6, 51 | "ltc_rur": 5, 52 | "ltc_eur": 3, 53 | "nmc_btc": 5, 54 | "nmc_usd": 3, 55 | "nvc_btc": 5, 56 | "nvc_usd": 3, 57 | "usd_rur": 5, 58 | "eur_usd": 5, 59 | "eur_rur": 5, 60 | "ppc_btc": 5, 61 | "ppc_usd": 3, 62 | "dsh_btc": 6, 63 | "eth_btc": 5, 64 | "eth_usd": 3, 65 | "eth_ltc": 5} 66 | 67 | min_orders = {"btc_usd": decimal.Decimal("0.01"), 68 | "btc_rur": decimal.Decimal("0.01"), 69 | "btc_eur": decimal.Decimal("0.01"), 70 | "ltc_btc": decimal.Decimal("0.1"), 71 | "ltc_usd": decimal.Decimal("0.1"), 72 | "ltc_rur": decimal.Decimal("0.1"), 73 | "ltc_eur": decimal.Decimal("0.1"), 74 | "nmc_btc": decimal.Decimal("0.1"), 75 | "nmc_usd": decimal.Decimal("0.1"), 76 | "nvc_btc": decimal.Decimal("0.1"), 77 | "nvc_usd": decimal.Decimal("0.1"), 78 | "usd_rur": decimal.Decimal("0.1"), 79 | "eur_usd": decimal.Decimal("0.1"), 80 | "eur_rur": decimal.Decimal("0.1"), 81 | "ppc_btc": decimal.Decimal("0.1"), 82 | "ppc_usd": decimal.Decimal("0.1"), 83 | "dsh_btc": decimal.Decimal("0.1"), 84 | "eth_btc": decimal.Decimal("0.1"), 85 | "eth_usd": decimal.Decimal("0.1"), 86 | "eth_ltc": decimal.Decimal("0.1")} 87 | 88 | 89 | def parseJSONResponse(response): 90 | def parse_decimal(var): 91 | return decimal.Decimal(var) 92 | 93 | try: 94 | r = json.loads(response, parse_float=parse_decimal, 95 | parse_int=parse_decimal) 96 | except Exception as e: 97 | msg = "Error while attempting to parse JSON response:" \ 98 | " %s\nResponse:\n%r" % (e, response) 99 | raise Exception(msg) 100 | 101 | return r 102 | 103 | 104 | HEADER_COOKIE_RE = re.compile(r'__cfduid=([a-f0-9]{46})') 105 | BODY_COOKIE_RE = re.compile(r'document\.cookie="a=([a-f0-9]{32});path=/;";') 106 | 107 | 108 | class BTCEConnection: 109 | def __init__(self, timeout=30): 110 | self._timeout = timeout 111 | self.setup_connection() 112 | 113 | def setup_connection(self): 114 | if ("HTTPS_PROXY" in os.environ): 115 | match = re.search(r'http://([\w.]+):(\d+)',os.environ['HTTPS_PROXY']) 116 | if match: 117 | self.conn = httplib.HTTPSConnection(match.group(1), 118 | port=match.group(2), 119 | timeout=self._timeout) 120 | self.conn.set_tunnel(btce_domain) 121 | else: 122 | self.conn = httplib.HTTPSConnection(btce_domain, timeout=self._timeout) 123 | self.cookie = None 124 | 125 | def close(self): 126 | self.conn.close() 127 | 128 | def getCookie(self): 129 | self.cookie = "" 130 | 131 | try: 132 | self.conn.request("GET", '/') 133 | response = self.conn.getresponse() 134 | except Exception: 135 | # reset connection so it doesn't stay in a weird state if we catch 136 | # the error in some other place 137 | self.conn.close() 138 | self.setup_connection() 139 | raise 140 | 141 | setCookieHeader = response.getheader("Set-Cookie") 142 | match = HEADER_COOKIE_RE.search(setCookieHeader) 143 | if match: 144 | self.cookie = "__cfduid=" + match.group(1) 145 | 146 | match = BODY_COOKIE_RE.search(response.read()) 147 | if match: 148 | if self.cookie != "": 149 | self.cookie += '; ' 150 | self.cookie += "a=" + match.group(1) 151 | 152 | def makeRequest(self, url, extra_headers=None, params="", with_cookie=False): 153 | headers = {"Content-type": "application/x-www-form-urlencoded"} 154 | if extra_headers is not None: 155 | headers.update(extra_headers) 156 | 157 | if with_cookie: 158 | if self.cookie is None: 159 | self.getCookie() 160 | 161 | headers.update({"Cookie": self.cookie}) 162 | 163 | try: 164 | self.conn.request("POST", url, params, headers) 165 | response = self.conn.getresponse() 166 | 167 | if response.status < 200 or response.status > 299: 168 | msg = "API response error: %s".format(response.status) 169 | raise APIResponseError(msg) 170 | except Exception: 171 | # reset connection so it doesn't stay in a weird state if we catch 172 | # the error in some other place 173 | self.conn.close() 174 | self.setup_connection() 175 | raise 176 | 177 | return response.read() 178 | 179 | def makeJSONRequest(self, url, extra_headers=None, params=""): 180 | response = self.makeRequest(url, extra_headers, params) 181 | return parseJSONResponse(response) 182 | 183 | 184 | def validatePair(pair): 185 | if pair not in all_pairs: 186 | if "_" in pair: 187 | a, b = pair.split("_", 1) 188 | swapped_pair = "%s_%s" % (b, a) 189 | if swapped_pair in all_pairs: 190 | msg = "Unrecognized pair: %r (did you mean %s?)" 191 | msg = msg % (pair, swapped_pair) 192 | raise InvalidTradePairException(msg) 193 | raise InvalidTradePairException("Unrecognized pair: %r" % pair) 194 | 195 | 196 | def validateOrder(pair, trade_type, rate, amount): 197 | validatePair(pair) 198 | if trade_type not in ("buy", "sell"): 199 | raise InvalidTradeTypeException("Unrecognized trade type: %r" % trade_type) 200 | 201 | minimum_amount = min_orders[pair] 202 | formatted_min_amount = formatCurrency(minimum_amount, pair) 203 | if amount < minimum_amount: 204 | msg = "Trade amount %r too small; should be >= %s" % \ 205 | (amount, formatted_min_amount) 206 | raise InvalidTradeAmountException(msg) 207 | 208 | 209 | def truncateAmountDigits(value, digits): 210 | quantum = exps[digits] 211 | if type(value) is float: 212 | value = str(value) 213 | if type(value) is str: 214 | value = decimal.Decimal(value) 215 | return value.quantize(quantum) 216 | 217 | 218 | def truncateAmount(value, pair): 219 | return truncateAmountDigits(value, max_digits[pair]) 220 | 221 | 222 | def formatCurrencyDigits(value, digits): 223 | s = str(truncateAmountDigits(value, digits)) 224 | s = s.rstrip("0") 225 | if s[-1] == ".": 226 | s = "%s0" % s 227 | 228 | return s 229 | 230 | 231 | def formatCurrency(value, pair): 232 | return formatCurrencyDigits(value, max_digits[pair]) 233 | -------------------------------------------------------------------------------- /btceapi/keyhandler.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2015 Alan McIntyre 2 | 3 | import warnings 4 | 5 | 6 | class InvalidNonceException(Exception): 7 | '''Exception raised when an invalid nonce is set on a key.''' 8 | pass 9 | 10 | 11 | class KeyData(object): 12 | def __init__(self, secret, nonce): 13 | self.secret = secret 14 | self.nonce = nonce 15 | 16 | # BTC-e's API caps nonces' values 17 | MAX_NONCE_VALUE = 4294967294 18 | 19 | def setNonce(self, newNonce): 20 | if newNonce <= 0: 21 | raise InvalidNonceException('Nonces must be positive') 22 | if newNonce <= self.nonce: 23 | raise InvalidNonceException('Nonces must be strictly increasing') 24 | if newNonce > self.MAX_NONCE_VALUE: 25 | raise InvalidNonceException('Nonces cannot be greater than %d' % 26 | self.MAX_NONCE_VALUE) 27 | 28 | self.nonce = newNonce 29 | 30 | return self.nonce 31 | 32 | def incrementNonce(self): 33 | if self.nonce >= self.MAX_NONCE_VALUE: 34 | raise InvalidNonceException('Cannot increment nonce, already at' 35 | ' maximum value') 36 | 37 | self.nonce += 1 38 | 39 | return self.nonce 40 | 41 | 42 | class AbstractKeyHandler(object): 43 | '''AbstractKeyHandler handles the tedious task of managing nonces 44 | associated with BTC-e API key/secret pairs. 45 | The getNextNonce method is threadsafe, all others need not be.''' 46 | def __init__(self): 47 | self._keys = {} 48 | self._loadKeys() 49 | 50 | @property 51 | def keys(self): 52 | return self._keys.keys() 53 | 54 | def getKeys(self): 55 | return self.keys 56 | 57 | # Should load the keys with their secrets and nonces from the datastore 58 | def _loadKeys(self): 59 | raise NotImplementedError 60 | 61 | # Should update the datastore with the latest data (newest nonces, and any 62 | # keys that might have been added) 63 | def _updateDatastore(self): 64 | raise NotImplementedError 65 | 66 | def __del__(self): 67 | self.close() 68 | 69 | def close(self): 70 | self._updateDatastore() 71 | 72 | def __enter__(self): 73 | return self 74 | 75 | def __exit__(self, *_args): 76 | self.close() 77 | 78 | def addKey(self, key, secret, next_nonce): 79 | self._keys[key] = KeyData(secret, next_nonce) 80 | return self 81 | 82 | def getNextNonce(self, key): 83 | return self.getKey(key).incrementNonce() 84 | 85 | def getSecret(self, key): 86 | return self.getKey(key).secret 87 | 88 | def setNextNonce(self, key, nextNonce): 89 | return self.getKey(key).setNonce(nextNonce) 90 | 91 | def getKey(self, key): 92 | data = self._keys.get(key) 93 | if data is None: 94 | raise KeyError("Key not found: %r" % key) 95 | 96 | return data 97 | 98 | 99 | class KeyHandler(AbstractKeyHandler): 100 | '''An implementation of AbstractKeyHandler using local files to store the 101 | data.''' 102 | def __init__(self, filename=None, resaveOnDeletion=True): 103 | '''The given file is assumed to be a text file with three lines 104 | (key, secret, nonce) per entry.''' 105 | if not resaveOnDeletion: 106 | warnings.warn("The resaveOnDeletion argument to KeyHandler will" 107 | " default to True in future versions.") 108 | 109 | self.resaveOnDeletion = resaveOnDeletion 110 | self.filename = filename 111 | super(KeyHandler, self).__init__() 112 | 113 | def _loadKeys(self): 114 | if self.filename is not None: 115 | self._parse() 116 | 117 | def _updateDatastore(self): 118 | if self.resaveOnDeletion and self.filename is not None: 119 | self._save() 120 | 121 | def _save(self): 122 | with open(self.filename, 'wt') as file: 123 | for k, data in self._keys.iteritems(): 124 | file.write("%s\n%s\n%d\n" % (k, data.secret, data.nonce)) 125 | 126 | def _parse(self): 127 | with open(self.filename, 'rt') as input_file: 128 | while True: 129 | key = input_file.readline().strip() 130 | if not key: 131 | break 132 | secret = input_file.readline().strip() 133 | nonce = int(input_file.readline().strip()) 134 | self.addKey(key, secret, nonce) 135 | -------------------------------------------------------------------------------- /btceapi/public.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2015 Alan McIntyre 2 | 3 | import datetime 4 | import decimal 5 | 6 | from btceapi import common 7 | 8 | 9 | def getTradeFee(pair, connection=None): 10 | """ 11 | Retrieve the fee (in percent) associated with trades for a given pair. 12 | """ 13 | 14 | common.validatePair(pair) 15 | 16 | if connection is None: 17 | connection = common.BTCEConnection() 18 | 19 | fees = connection.makeJSONRequest("/api/2/%s/fee" % pair) 20 | if type(fees) is not dict: 21 | raise TypeError("The response is not a dict.") 22 | 23 | trade_fee = fees.get(u'trade') 24 | if type(trade_fee) is not decimal.Decimal: 25 | raise TypeError("The response does not contain a trade fee") 26 | 27 | return trade_fee 28 | 29 | 30 | class Ticker(object): 31 | __slots__ = ('high', 'low', 'avg', 'vol', 'vol_cur', 'last', 'buy', 'sell', 32 | 'updated', 'server_time') 33 | 34 | def __init__(self, **kwargs): 35 | for s in Ticker.__slots__: 36 | setattr(self, s, kwargs.get(s)) 37 | 38 | self.updated = datetime.datetime.fromtimestamp(self.updated) 39 | self.server_time = datetime.datetime.fromtimestamp(self.server_time) 40 | 41 | def __getstate__(self): 42 | return dict((k, getattr(self, k)) for k in Ticker.__slots__) 43 | 44 | def __setstate__(self, state): 45 | for k, v in state.items(): 46 | setattr(self, k, v) 47 | 48 | 49 | def getTicker(pair, connection=None): 50 | """Retrieve the ticker for the given pair. Returns a Ticker instance.""" 51 | 52 | common.validatePair(pair) 53 | 54 | if connection is None: 55 | connection = common.BTCEConnection() 56 | 57 | response = connection.makeJSONRequest("/api/2/%s/ticker" % pair) 58 | 59 | if type(response) is not dict: 60 | raise TypeError("The response is a %r, not a dict." % type(response)) 61 | elif u'error' in response: 62 | print ("There is a error \"%s\" while obtaining ticker %s" % (response['error'], pair)) 63 | ticker = None 64 | else: 65 | ticker = Ticker(**response[u'ticker']) 66 | 67 | return ticker 68 | 69 | 70 | def getDepth(pair, connection=None): 71 | """Retrieve the depth for the given pair. Returns a tuple (asks, bids); 72 | each of these is a list of (price, volume) tuples.""" 73 | 74 | common.validatePair(pair) 75 | 76 | if connection is None: 77 | connection = common.BTCEConnection() 78 | 79 | depth = connection.makeJSONRequest("/api/2/%s/depth" % pair) 80 | if type(depth) is not dict: 81 | raise TypeError("The response is not a dict.") 82 | 83 | asks = depth.get(u'asks') 84 | if type(asks) is not list: 85 | raise TypeError("The response does not contain an asks list.") 86 | 87 | bids = depth.get(u'bids') 88 | if type(bids) is not list: 89 | raise TypeError("The response does not contain a bids list.") 90 | 91 | return asks, bids 92 | 93 | 94 | class Trade(object): 95 | __slots__ = ('pair', 'trade_type', 'price', 'tid', 'amount', 'date') 96 | 97 | def __init__(self, **kwargs): 98 | for s in Trade.__slots__: 99 | setattr(self, s, kwargs.get(s)) 100 | 101 | if type(self.date) in (int, float, decimal.Decimal): 102 | self.date = datetime.datetime.fromtimestamp(self.date) 103 | elif type(self.date) in (str, unicode): 104 | if "." in self.date: 105 | self.date = datetime.datetime.strptime(self.date, 106 | "%Y-%m-%d %H:%M:%S.%f") 107 | else: 108 | self.date = datetime.datetime.strptime(self.date, 109 | "%Y-%m-%d %H:%M:%S") 110 | 111 | def __getstate__(self): 112 | return dict((k, getattr(self, k)) for k in Trade.__slots__) 113 | 114 | def __setstate__(self, state): 115 | for k, v in state.items(): 116 | setattr(self, k, v) 117 | 118 | 119 | def getTradeHistory(pair, connection=None, count=None): 120 | """Retrieve the trade history for the given pair. Returns a list of 121 | Trade instances. If count is not None, it should be an integer, and 122 | specifies the number of items from the trade history that will be 123 | processed and returned.""" 124 | 125 | common.validatePair(pair) 126 | 127 | if connection is None: 128 | connection = common.BTCEConnection() 129 | 130 | history = connection.makeJSONRequest("/api/2/%s/trades" % pair) 131 | 132 | if type(history) is not list: 133 | raise TypeError("The response is a %r, not a list." % type(history)) 134 | 135 | result = [] 136 | 137 | # Limit the number of items returned if requested. 138 | if count is not None: 139 | history = history[:count] 140 | 141 | for h in history: 142 | h["pair"] = pair 143 | t = Trade(**h) 144 | result.append(t) 145 | return result 146 | -------------------------------------------------------------------------------- /btceapi/scraping.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2015 Alan McIntyre 2 | 3 | from HTMLParser import HTMLParser 4 | import datetime 5 | import warnings 6 | from btceapi.common import BTCEConnection, all_pairs 7 | 8 | 9 | class BTCEScraper(HTMLParser): 10 | def __init__(self): 11 | HTMLParser.__init__(self) 12 | self.messageId = None 13 | self.messageTime = None 14 | self.messageUser = None 15 | self.messageText = None 16 | self.messages = [] 17 | 18 | self.inMessageA = False 19 | self.inMessageSpan = False 20 | 21 | self.devOnline = False 22 | self.supportOnline = False 23 | self.adminOnline = False 24 | 25 | def handle_data(self, data): 26 | # Capture contents of and tags, which contain 27 | # the user ID and the message text, respectively. 28 | if self.inMessageA: 29 | self.messageUser = data.strip() 30 | elif self.inMessageSpan: 31 | self.messageText = data.strip() 32 | 33 | def handle_starttag(self, tag, attrs): 34 | if tag == 'p': 35 | # Check whether this

tag has id="msgXXXXXX" and 36 | # class="chatmessage"; if not, it doesn't contain a message. 37 | messageId = None 38 | for k, v in attrs: 39 | if k == 'id': 40 | if v[:3] != 'msg': 41 | return 42 | messageId = v 43 | if k == 'class' and v != 'chatmessage': 44 | return 45 | 46 | # This appears to be a message

tag, so set the message ID. 47 | # Other code in this class assumes that if self.messageId is None, 48 | # the tags being processed are not relevant. 49 | if messageId is not None: 50 | self.messageId = messageId 51 | elif tag == 'a': 52 | if self.messageId is not None: 53 | # Check whether this tag has class="chatmessage" and a 54 | # time string in the title attribute; if not, it's not part 55 | # of a message. 56 | messageTime = None 57 | for k, v in attrs: 58 | if k == 'title': 59 | messageTime = v 60 | if k == 'class' and v != 'chatmessage': 61 | return 62 | 63 | if messageTime is None: 64 | return 65 | 66 | # This appears to be a message tag, so remember the message 67 | # time and set the inMessageA flag so the tag's data can be 68 | # captured in the handle_data method. 69 | self.inMessageA = True 70 | self.messageTime = messageTime 71 | else: 72 | for k, v in attrs: 73 | if k != 'href': 74 | continue 75 | 76 | # If the tag for dev/support/admin is present, then 77 | # they are online (otherwise nothing appears on the 78 | # page for them). 79 | if v == 'https://btc-e.com/profile/1': 80 | self.devOnline = True 81 | elif v == 'https://btc-e.com/profile/2': 82 | self.supportOnline = True 83 | elif v == 'https://btc-e.com/profile/3': 84 | self.adminOnline = True 85 | elif tag == 'span': 86 | if self.messageId is not None: 87 | self.inMessageSpan = True 88 | 89 | def handle_endtag(self, tag): 90 | if tag == 'p' and self.messageId is not None: 91 | # exiting from the message

tag 92 | 93 | # check for invalid message contents 94 | if self.messageId is None: 95 | warnings.warn("Missing message ID") 96 | if self.messageUser is None: 97 | warnings.warn("Missing message user") 98 | if self.messageTime is None: 99 | warnings.warn("Missing message time") 100 | 101 | if self.messageText is None: 102 | # messageText will be None if the message consists entirely 103 | # of emoticons. 104 | self.messageText = '' 105 | 106 | # parse message time 107 | t = datetime.datetime.now() 108 | messageTime = t.strptime(self.messageTime, '%d.%m.%y %H:%M:%S') 109 | 110 | self.messages.append((self.messageId, self.messageUser, 111 | messageTime, self.messageText)) 112 | self.messageId = None 113 | self.messageUser = None 114 | self.messageTime = None 115 | self.messageText = None 116 | elif tag == 'a' and self.messageId is not None: 117 | self.inMessageA = False 118 | elif tag == 'span': 119 | self.inMessageSpan = False 120 | 121 | 122 | class ScraperResults(object): 123 | __slots__ = ('messages', 'devOnline', 'supportOnline', 'adminOnline') 124 | 125 | def __init__(self): 126 | self.messages = None 127 | self.devOnline = False 128 | self.supportOnline = False 129 | self.adminOnline = False 130 | 131 | def __getstate__(self): 132 | return dict((k, getattr(self, k)) for k in ScraperResults.__slots__) 133 | 134 | def __setstate__(self, state): 135 | for k, v in state.items(): 136 | setattr(self, k, v) 137 | 138 | 139 | _current_pair_index = 0 140 | 141 | 142 | def scrapeMainPage(connection=None): 143 | if connection is None: 144 | connection = BTCEConnection() 145 | 146 | parser = BTCEScraper() 147 | 148 | # Rotate through the currency pairs between chat requests so that the 149 | # chat pane contents will update more often than every few minutes. 150 | global _current_pair_index 151 | _current_pair_index = (_current_pair_index + 1) % len(all_pairs) 152 | current_pair = all_pairs[_current_pair_index] 153 | 154 | response = connection.makeRequest('/exchange/%s' % current_pair, 155 | with_cookie=True) 156 | 157 | parser.feed(parser.unescape(response.decode('utf-8'))) 158 | parser.close() 159 | 160 | r = ScraperResults() 161 | r.messages = parser.messages 162 | r.devOnline = parser.devOnline 163 | r.supportOnline = parser.supportOnline 164 | r.adminOnline = parser.adminOnline 165 | 166 | return r 167 | -------------------------------------------------------------------------------- /btceapi/trade.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2015 Alan McIntyre 2 | 3 | import urllib 4 | import hashlib 5 | import hmac 6 | import warnings 7 | from datetime import datetime 8 | from btceapi import common 9 | from btceapi import keyhandler 10 | 11 | 12 | class InvalidNonceException(Exception): 13 | def __init__(self, method, expectedNonce, actualNonce): 14 | Exception.__init__(self) 15 | self.method = method 16 | self.expectedNonce = expectedNonce 17 | self.actualNonce = actualNonce 18 | 19 | def __str__(self): 20 | return "Expected a nonce greater than %d" % self.expectedNonce 21 | 22 | 23 | class InvalidSortOrderException(Exception): 24 | ''' Exception thrown when an invalid sort order is passed ''' 25 | pass 26 | 27 | 28 | class TradeAccountInfo(object): 29 | '''An instance of this class will be returned by 30 | a successful call to TradeAPI.getInfo.''' 31 | 32 | def __init__(self, info): 33 | funds = info.get(u'funds') 34 | for c in common.all_currencies: 35 | setattr(self, "balance_%s" % c, funds.get(unicode(c), 0)) 36 | 37 | self.open_orders = info.get(u'open_orders') 38 | self.server_time = datetime.fromtimestamp(info.get(u'server_time')) 39 | 40 | self.transaction_count = info.get(u'transaction_count') 41 | rights = info.get(u'rights') 42 | self.info_rights = (rights.get(u'info') == 1) 43 | self.withdraw_rights = (rights.get(u'withdraw') == 1) 44 | self.trade_rights = (rights.get(u'trade') == 1) 45 | 46 | 47 | class TransactionHistoryItem(object): 48 | '''A list of instances of this class will be returned by 49 | a successful call to TradeAPI.transHistory.''' 50 | 51 | def __init__(self, transaction_id, info): 52 | self.transaction_id = transaction_id 53 | items = ("type", "amount", "currency", "desc", 54 | "status", "timestamp") 55 | for n in items: 56 | setattr(self, n, info.get(n)) 57 | self.timestamp = datetime.fromtimestamp(self.timestamp) 58 | 59 | 60 | class TradeHistoryItem(object): 61 | '''A list of instances of this class will be returned by 62 | a successful call to TradeAPI.tradeHistory.''' 63 | 64 | def __init__(self, transaction_id, info): 65 | self.transaction_id = transaction_id 66 | items = ("pair", "type", "amount", "rate", "order_id", 67 | "is_your_order", "timestamp") 68 | for n in items: 69 | setattr(self, n, info.get(n)) 70 | self.timestamp = datetime.fromtimestamp(self.timestamp) 71 | 72 | 73 | class OrderItem(object): 74 | '''A list of instances of this class will be returned by 75 | a successful call to TradeAPI.activeOrders.''' 76 | 77 | def __init__(self, order_id, info): 78 | self.order_id = int(order_id) 79 | vnames = ("pair", "type", "amount", "rate", "timestamp_created", 80 | "status") 81 | for n in vnames: 82 | setattr(self, n, info.get(n)) 83 | self.timestamp_created = datetime.fromtimestamp(self.timestamp_created) 84 | 85 | 86 | class TradeResult(object): 87 | '''An instance of this class will be returned by 88 | a successful call to TradeAPI.trade.''' 89 | 90 | def __init__(self, info): 91 | self.received = info.get(u"received") 92 | self.remains = info.get(u"remains") 93 | self.order_id = info.get(u"order_id") 94 | funds = info.get(u'funds') 95 | for c in common.all_currencies: 96 | setattr(self, "balance_%s" % c, funds.get(unicode(c), 0)) 97 | 98 | 99 | class CancelOrderResult(object): 100 | '''An instance of this class will be returned by 101 | a successful call to TradeAPI.cancelOrder.''' 102 | 103 | def __init__(self, info): 104 | self.order_id = info.get(u"order_id") 105 | funds = info.get(u'funds') 106 | for c in common.all_currencies: 107 | setattr(self, "balance_%s" % c, funds.get(unicode(c), 0)) 108 | 109 | 110 | def setHistoryParams(params, from_number, count_number, from_id, end_id, 111 | order, since, end): 112 | if from_number is not None: 113 | params["from"] = "%d" % from_number 114 | if count_number is not None: 115 | params["count"] = "%d" % count_number 116 | if from_id is not None: 117 | params["from_id"] = "%d" % from_id 118 | if end_id is not None: 119 | params["end_id"] = "%d" % end_id 120 | if order is not None: 121 | if order not in ("ASC", "DESC"): 122 | raise InvalidSortOrderException("Unexpected order parameter: %r" % order) 123 | params["order"] = order 124 | if since is not None: 125 | params["since"] = "%d" % since 126 | if end is not None: 127 | params["end"] = "%d" % end 128 | 129 | 130 | class TradeAPI(object): 131 | def __init__(self, key, handler): 132 | self.key = key 133 | self.handler = handler 134 | 135 | if not isinstance(self.handler, keyhandler.AbstractKeyHandler): 136 | raise TypeError("The handler argument must be a" 137 | " keyhandler.AbstractKeyHandler, such as" 138 | " keyhandler.KeyHandler") 139 | 140 | # We depend on the key handler for the secret 141 | self.secret = handler.getSecret(key) 142 | 143 | def _post(self, params, connection=None, raiseIfInvalidNonce=False): 144 | params["nonce"] = self.handler.getNextNonce(self.key) 145 | encoded_params = urllib.urlencode(params) 146 | 147 | # Hash the params string to produce the Sign header value 148 | H = hmac.new(self.secret, digestmod=hashlib.sha512) 149 | H.update(encoded_params) 150 | sign = H.hexdigest() 151 | 152 | if connection is None: 153 | connection = common.BTCEConnection() 154 | 155 | headers = {"Key": self.key, "Sign": sign} 156 | result = connection.makeJSONRequest("/tapi", headers, encoded_params) 157 | 158 | success = result.get(u'success') 159 | if not success: 160 | err_message = result.get(u'error') 161 | method = params.get("method", "[uknown method]") 162 | 163 | if "invalid nonce" in err_message: 164 | # If the nonce is out of sync, make one attempt to update to 165 | # the correct nonce. This sometimes happens if a bot crashes 166 | # and the nonce file doesn't get saved, so it's reasonable to 167 | # attempt one correction. If multiple threads/processes are 168 | # attempting to use the same key, this mechanism will 169 | # eventually fail and the InvalidNonce will be emitted so that 170 | # you'll end up here reading this comment. :) 171 | 172 | # The assumption is that the invalid nonce message looks like 173 | # "invalid nonce parameter; on key:4, you sent:3" 174 | s = err_message.split(",") 175 | expected = int(s[-2].split(":")[1].strip("'")) 176 | actual = int(s[-1].split(":")[1].strip("'")) 177 | if raiseIfInvalidNonce: 178 | raise InvalidNonceException(method, expected, actual) 179 | 180 | warnings.warn("The nonce in the key file is out of date;" 181 | " attempting to correct.") 182 | self.handler.setNextNonce(self.key, expected + 1) 183 | return self._post(params, connection, True) 184 | elif "no orders" in err_message and method == "ActiveOrders": 185 | # ActiveOrders returns failure if there are no orders; 186 | # intercept this and return an empty dict. 187 | return {} 188 | elif "no trades" in err_message and method == "TradeHistory": 189 | # TradeHistory returns failure if there are no trades; 190 | # intercept this and return an empty dict. 191 | return {} 192 | 193 | raise Exception("%s call failed with error: %s" 194 | % (method, err_message)) 195 | 196 | if u'return' not in result: 197 | raise Exception("Response does not contain a 'return' item.") 198 | 199 | return result.get(u'return') 200 | 201 | def getInfo(self, connection=None): 202 | params = {"method": "getInfo"} 203 | return TradeAccountInfo(self._post(params, connection)) 204 | 205 | def transHistory(self, from_number=None, count_number=None, 206 | from_id=None, end_id=None, order="DESC", 207 | since=None, end=None, connection=None): 208 | 209 | params = {"method": "TransHistory"} 210 | 211 | setHistoryParams(params, from_number, count_number, from_id, end_id, 212 | order, since, end) 213 | 214 | orders = self._post(params, connection) 215 | result = [] 216 | for k, v in orders.items(): 217 | result.append(TransactionHistoryItem(int(k), v)) 218 | 219 | # We have to sort items here because the API returns a dict 220 | if "ASC" == order: 221 | result.sort(key=lambda a: a.transaction_id, reverse=False) 222 | elif "DESC" == order: 223 | result.sort(key=lambda a: a.transaction_id, reverse=True) 224 | 225 | return result 226 | 227 | def tradeHistory(self, from_number=None, count_number=None, 228 | from_id=None, end_id=None, order=None, 229 | since=None, end=None, pair=None, connection=None): 230 | 231 | params = {"method": "TradeHistory"} 232 | 233 | setHistoryParams(params, from_number, count_number, from_id, end_id, 234 | order, since, end) 235 | 236 | if pair is not None: 237 | common.validatePair(pair) 238 | params["pair"] = pair 239 | 240 | orders = list(self._post(params, connection).items()) 241 | orders.sort(reverse=order != "ASC") 242 | result = [] 243 | for k, v in orders: 244 | result.append(TradeHistoryItem(int(k), v)) 245 | 246 | return result 247 | 248 | def activeOrders(self, pair=None, connection=None): 249 | 250 | params = {"method": "ActiveOrders"} 251 | 252 | if pair is not None: 253 | common.validatePair(pair) 254 | params["pair"] = pair 255 | 256 | orders = self._post(params, connection) 257 | result = [] 258 | for k, v in orders.items(): 259 | result.append(OrderItem(k, v)) 260 | 261 | return result 262 | 263 | def trade(self, pair, trade_type, rate, amount, connection=None): 264 | common.validateOrder(pair, trade_type, rate, amount) 265 | params = {"method": "Trade", 266 | "pair": pair, 267 | "type": trade_type, 268 | "rate": common.formatCurrency(rate, pair), 269 | "amount": common.formatCurrency(amount, pair)} 270 | 271 | return TradeResult(self._post(params, connection)) 272 | 273 | def cancelOrder(self, order_id, connection=None): 274 | params = {"method": "CancelOrder", 275 | "order_id": order_id} 276 | return CancelOrderResult(self._post(params, connection)) 277 | -------------------------------------------------------------------------------- /build_brokers.py: -------------------------------------------------------------------------------- 1 | from Broker import Broker 2 | from btce import BTCE 3 | import logging 4 | 5 | def build_brokers(mode, pairs, exchanges): 6 | brokers = [] 7 | # Returns array of broker objects 8 | for e in exchanges: 9 | if e == 'BTCE': 10 | exchange = BTCE('./btce_keys.txt') 11 | elif e == 'GDAX': 12 | exchange = GDAX() 13 | elif e == 'POLONIEX': 14 | exchange = POLONIEX() 15 | else: 16 | logging.error("This exchange is not yet supported : " + e) 17 | 18 | broker = Broker(mode, exchange) 19 | brokers.append(broker) 20 | 21 | return brokers -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | EXCHANGES = ['BTCE'] 2 | # , 'BITFINEX', 'POLOINEX', 'BTCC','GDAX' 3 | 4 | # BTCC Can only do LTC BTC 5 | PAIRS = [("BTC","LTC"),("BTC","ETH")] 6 | 7 | BALLPARK_VALUE = { 8 | "BTC" : 580, 9 | "LTC" : 3, 10 | "ETH" : 12 11 | } # Ballpark value of each currency in USD 12 | 13 | MINIMUM_PROFIT_VOLUME = { key : 0.01/value for (key, value) in BALLPARK_VALUE.iteritems()} # {"currency" : "minimum profit} we want the equivalent of 0.01 dollars 14 | 15 | TRADE_MODE = "paper" # paper or real 16 | 17 | # Initial balance of each currency, used in paper trading mode 18 | INITIAL_BALANCE = { 19 | "BTC" : 0, 20 | "LTC" : 0, 21 | "ETH" : 0 22 | } -------------------------------------------------------------------------------- /config_key.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suhithr/bitcoin-arbitrage-bot/18fad6a7e7e32e4752eb15b714c5cbe672941ab6/config_key.py -------------------------------------------------------------------------------- /poloniex.py: -------------------------------------------------------------------------------- 1 | # Under construction 2 | from Exchange import Exchange 3 | import poloniex 4 | import config_key 5 | 6 | class POLONIEX(Exchange): 7 | def __init__(self): 8 | self.name = 'POLONIEX' 9 | self.public_api = poloniex.Poloniex() 10 | self.private_api = poloniex.Poloniex(config_key.poloniex_api_key, config_key.poloniex_secret_key) 11 | # This is the taker fee for a 30 day volume of <600BTC 12 | # in this arbitrage strategy we do not make orders, only fill(take) existing ones thus we apply the taker fee 13 | self.trading_fee = 0.25 14 | 15 | def get_tradeable_pairs(self): 16 | tradeable_pairs = [] 17 | # we just fetch the 24hour volume for all markets and read keys 18 | vol = self.public_api.marketVolume() 19 | for key in vol: 20 | a, b = key.split("_") 21 | tradeable_pairs.append((a.upper(), b.upper())) 22 | return tradeable_pairs 23 | 24 | def get_depth(self, base, alt): 25 | order_book = {'bids': [], 'asks': []} 26 | pair, swapped = self.get_validated_pair((base, alt)) 27 | if pair is None: 28 | return 29 | 30 | pairstr = pair[0].upper() + "_" + pair[1].upper() 31 | # Get orderbook to a depth of 150 since that's the default in the BTCE API 32 | if swapped: 33 | bids, asks = self.public_api.marketOrders(pairstr, 150) 34 | else: 35 | asks, bids = self.public_api.marketOrders(pairstr, 150) 36 | 37 | def get_min_vol(self, pair, depth): 38 | base, alt = pair 39 | -------------------------------------------------------------------------------- /poloniex/__init__.py: -------------------------------------------------------------------------------- 1 | # Poloniex API wrapper tested on Python 2.7.6 & 3.4.3 2 | # https://github.com/s4w3d0ff/python-poloniex 3 | # BTC: 15D8VaZco22GTLVrFMAehXyif6EGf8GMYV 4 | # TODO: 5 | # [x] PEP8 6 | # [ ] Add better logger access 7 | # [ ] Find out if request module has the equivalent to urlencode 8 | # [ ] Add Push Api application wrapper 9 | # [ ] Convert docstrings to sphinx 10 | # 11 | # Copyright (C) 2016 https://github.com/s4w3d0ff 12 | # 13 | # This program is free software; you can redistribute it and/or modify 14 | # it under the terms of the GNU General Public License as published by 15 | # the Free Software Foundation; either version 2 of the License, or 16 | # (at your option) any later version. 17 | # 18 | # This program is distributed in the hope that it will be useful, 19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 | # GNU General Public License for more details. 22 | # 23 | # You should have received a copy of the GNU General Public License along 24 | # with this program; if not, write to the Free Software Foundation, Inc., 25 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 26 | import sys 27 | import time 28 | import calendar 29 | import logging 30 | import json 31 | import hmac 32 | import hashlib 33 | # pip 34 | import requests 35 | 36 | if sys.version_info[0] is 3: 37 | from urllib.parse import urlencode 38 | else: 39 | from urllib import urlencode 40 | 41 | # Possible Commands 42 | PUBLIC_COMMANDS = [ 43 | 'returnTicker', 44 | 'return24hVolume', 45 | 'returnOrderBook', 46 | 'returnTradeHistory', 47 | 'returnChartData', 48 | 'returnCurrencies', 49 | 'returnLoanOrders'] 50 | 51 | PRIVATE_COMMANDS = [ 52 | 'returnBalances', 53 | 'returnCompleteBalances', 54 | 'returnDepositAddresses', 55 | 'generateNewAddress', 56 | 'returnDepositsWithdrawals', 57 | 'returnOpenOrders', 58 | 'returnTradeHistory', 59 | 'returnAvailableAccountBalances', 60 | 'returnTradableBalances', 61 | 'returnOpenLoanOffers', 62 | 'returnOrderTrades', 63 | 'returnActiveLoans', 64 | 'createLoanOffer', 65 | 'cancelLoanOffer', 66 | 'toggleAutoRenew', 67 | 'buy', 68 | 'sell', 69 | 'cancelOrder', 70 | 'moveOrder', 71 | 'withdraw', 72 | 'returnFeeInfo', 73 | 'transferBalance', 74 | 'returnMarginAccountSummary', 75 | 'marginBuy', 76 | 'marginSell', 77 | 'getMarginPosition', 78 | 'closeMarginPosition'] 79 | 80 | 81 | class Poloniex(object): 82 | """The Poloniex Object!""" 83 | def __init__( 84 | self, APIKey=False, Secret=False, 85 | timeout=3, coach=False, loglevel=logging.WARNING): 86 | """ 87 | APIKey = str api key supplied by Poloniex 88 | Secret = str secret hash supplied by Poloniex 89 | timeout = int time in sec to wait for an api response 90 | (otherwise 'requests.exceptions.Timeout' is raised) 91 | coach = bool to indicate if the api coach should be used 92 | loglevel = logging level object to set the module at 93 | (changes the requests module as well) 94 | 95 | self.apiCoach = object that regulates spacing between api calls 96 | 97 | # Time Placeholders # (MONTH == 30*DAYS) 98 | 99 | self.MINUTE, self.HOUR, self.DAY, self.WEEK, self.MONTH, self.YEAR 100 | """ 101 | # Set wrapper logging level 102 | logging.basicConfig( 103 | format='[%(asctime)s] %(message)s', 104 | datefmt="%H:%M:%S", 105 | level=loglevel) 106 | # Suppress the requests module logging output 107 | logging.getLogger("requests").setLevel(loglevel) 108 | logging.getLogger("urllib3").setLevel(loglevel) 109 | # Call coach, set nonce 110 | self.apiCoach, self.nonce = [Coach(), int(time.time()*1000)] 111 | # Grab keys, set timeout, ditch coach? 112 | self.APIKey, self.Secret, self.timeout, self._coaching = \ 113 | [APIKey, Secret, timeout, coach] 114 | # Set time labels 115 | self.MINUTE, self.HOUR, self.DAY, self.WEEK, self.MONTH, self.YEAR = \ 116 | [60, 60*60, 60*60*24, 60*60*24*7, 60*60*24*30, 60*60*24*365] 117 | 118 | # -----------------Meat and Potatos--------------------------------------- 119 | def api(self, command, args={}): 120 | """ 121 | Main Api Function 122 | - encodes and sends with optional [args] to Poloniex api 123 | - raises 'ValueError' if an api key or secret is missing 124 | (and the command is 'private'), or if the is not valid 125 | - returns decoded json api message 126 | """ 127 | global PUBLIC_COMMANDS, PRIVATE_COMMANDS 128 | 129 | # check in with the coach 130 | if self._coaching: 131 | self.apiCoach.wait() 132 | 133 | # pass the command 134 | args['command'] = command 135 | 136 | # private? 137 | if command in PRIVATE_COMMANDS: 138 | # check for keys 139 | if not self.APIKey or not self.Secret: 140 | raise ValueError("APIKey and Secret needed!") 141 | # set nonce 142 | args['nonce'] = self.nonce 143 | 144 | try: 145 | # encode arguments for url 146 | postData = urlencode(args) 147 | # sign postData with our Secret 148 | sign = hmac.new( 149 | self.Secret.encode('utf-8'), 150 | postData.encode('utf-8'), 151 | hashlib.sha512) 152 | # post request 153 | ret = requests.post( 154 | 'https://poloniex.com/tradingApi', 155 | data=args, 156 | headers={ 157 | 'Sign': sign.hexdigest(), 158 | 'Key': self.APIKey 159 | }, 160 | timeout=self.timeout) 161 | # return decoded json 162 | return json.loads(ret.text) 163 | 164 | except Exception as e: 165 | raise e 166 | 167 | finally: 168 | # increment nonce(no matter what) 169 | self.nonce += 1 170 | 171 | # public? 172 | elif command in PUBLIC_COMMANDS: 173 | try: 174 | ret = requests.post( 175 | 'https://poloniex.com/public?' + urlencode(args), 176 | timeout=self.timeout) 177 | return json.loads(ret.text) 178 | except Exception as e: 179 | raise e 180 | else: 181 | raise ValueError("Invalid Command!") 182 | 183 | # Convertions 184 | def epoch2UTCstr(self, timestamp=time.time(), fmat="%Y-%m-%d %H:%M:%S"): 185 | """ 186 | - takes epoch timestamp 187 | - returns UTC formated string 188 | """ 189 | return time.strftime(fmat, time.gmtime(timestamp)) 190 | 191 | def UTCstr2epoch(self, datestr=False, fmat="%Y-%m-%d %H:%M:%S"): 192 | """ 193 | - takes UTC date string 194 | - returns epoch 195 | """ 196 | if not datestr: 197 | datestr = self.epoch2UTCstr() 198 | return calendar.timegm(time.strptime(datestr, fmat)) 199 | 200 | def epoch2localstr(self, timestamp=time.time(), fmat="%Y-%m-%d %H:%M:%S"): 201 | """ 202 | - takes epoch timestamp 203 | - returns localtimezone formated string 204 | """ 205 | return time.strftime(fmat, time.localtime(timestamp)) 206 | 207 | def localstr2epoch(self, datestr=False, fmat="%Y-%m-%d %H:%M:%S"): 208 | """ 209 | - takes localtimezone date string, 210 | - returns epoch 211 | """ 212 | if not datestr: 213 | datestr = self.epoch2UTCstr() 214 | return time.mktime(time.strptime(datestr, fmat)) 215 | 216 | def float2roundPercent(self, floatN, decimalP=2): 217 | """ 218 | - takes float 219 | - returns percent(*100) rounded to the Nth decimal place as a string 220 | """ 221 | return str(round(float(floatN)*100, decimalP))+"%" 222 | 223 | # --PUBLIC COMMANDS------------------------------------------------------- 224 | def marketTicker(self): 225 | """ Returns the ticker for all markets """ 226 | return self.api('returnTicker') 227 | 228 | def marketVolume(self): 229 | """ Returns the volume data for all markets """ 230 | return self.api('return24hVolume') 231 | 232 | def marketStatus(self): 233 | """ Returns additional market info for all markets """ 234 | return self.api('returnCurrencies') 235 | 236 | def marketLoans(self, coin): 237 | """ Returns loan order book for """ 238 | return self.api('returnLoanOrders', {'currency': str(coin)}) 239 | 240 | def marketOrders(self, pair='all', depth=20): 241 | """ 242 | Returns orderbook for [pair='all'] 243 | at a depth of [depth=20] orders 244 | """ 245 | return self.api('returnOrderBook', { 246 | 'currencyPair': str(pair), 247 | 'depth': str(depth) 248 | }) 249 | 250 | def marketChart(self, pair, period=False, start=False, end=time.time()): 251 | """ 252 | Returns chart data for with a candle period of 253 | [period=self.DAY] starting from [start=time.time()-self.YEAR] 254 | and ending at [end=time.time()] 255 | """ 256 | if not period: 257 | period = self.DAY 258 | if not start: 259 | start = time.time()-(self.MONTH*2) 260 | return self.api('returnChartData', { 261 | 'currencyPair': str(pair), 262 | 'period': str(period), 263 | 'start': str(start), 264 | 'end': str(end) 265 | }) 266 | 267 | def marketTradeHist(self, pair, start=False, end=time.time()): 268 | """ 269 | Returns public trade history for 270 | starting at and ending at [end=time.time()] 271 | """ 272 | if self._coaching: 273 | self.apiCoach.wait() 274 | if not start: 275 | start = time.time()-self.HOUR 276 | try: 277 | ret = requests.post( 278 | 'https://poloniex.com/public?'+urlencode({ 279 | 'command': 'returnTradeHistory', 280 | 'currencyPair': str(pair), 281 | 'start': str(start), 282 | 'end': str(end) 283 | }), 284 | timeout=self.timeout) 285 | return json.loads(ret.text) 286 | except Exception as e: 287 | raise e 288 | 289 | # --PRIVATE COMMANDS------------------------------------------------------ 290 | def myTradeHist(self, pair): 291 | """ Returns private trade history for """ 292 | return self.api('returnTradeHistory', {'currencyPair': str(pair)}) 293 | 294 | def myBalances(self): 295 | """ Returns coin balances """ 296 | return self.api('returnBalances') 297 | 298 | def myAvailBalances(self): 299 | """ Returns available account balances """ 300 | return self.api('returnAvailableAccountBalances') 301 | 302 | def myMarginAccountSummary(self): 303 | """ Returns margin account summary """ 304 | return self.api('returnMarginAccountSummary') 305 | 306 | def myMarginPosition(self, pair='all'): 307 | """ Returns margin position for [pair='all'] """ 308 | return self.api('getMarginPosition', {'currencyPair': str(pair)}) 309 | 310 | def myCompleteBalances(self): 311 | """ Returns complete balances """ 312 | return self.api('returnCompleteBalances') 313 | 314 | def myAddresses(self): 315 | """ Returns deposit addresses """ 316 | return self.api('returnDepositAddresses') 317 | 318 | def myOrders(self, pair='all'): 319 | """ Returns your open orders for [pair='all'] """ 320 | return self.api('returnOpenOrders', {'currencyPair': str(pair)}) 321 | 322 | def myDepositsWithdraws(self): 323 | """ Returns deposit/withdraw history """ 324 | return self.api('returnDepositsWithdrawals') 325 | 326 | def myTradeableBalances(self): 327 | """ Returns tradable balances """ 328 | return self.api('returnTradableBalances') 329 | 330 | def myActiveLoans(self): 331 | """ Returns active loans """ 332 | return self.api('returnActiveLoans') 333 | 334 | def myOpenLoanOrders(self): 335 | """ Returns open loan offers """ 336 | return self.api('returnOpenLoanOffers') 337 | 338 | # --Trading functions-- # 339 | def orderTrades(self, orderId): 340 | """ Returns any trades made from """ 341 | return self.api('returnOrderTrades', {'orderNumber': str(orderId)}) 342 | 343 | def createLoanOrder(self, coin, amount, rate): 344 | """ Creates a loan offer for for at """ 345 | return self.api('createLoanOffer', { 346 | 'currency': str(coin), 347 | 'amount': str(amount), 348 | 'duration': str(2), 349 | 'autoRenew': str(0), 350 | 'lendingRate': str(rate) 351 | }) 352 | 353 | def cancelLoanOrder(self, orderId): 354 | """ Cancels the loan offer with """ 355 | return self.api('cancelLoanOffer', {'orderNumber': str(orderId)}) 356 | 357 | def toggleAutoRenew(self, orderId): 358 | """ Toggles the 'autorenew' feature on loan """ 359 | return self.api('toggleAutoRenew', {'orderNumber': str(orderId)}) 360 | 361 | def closeMarginPosition(self, pair): 362 | """ Closes the margin position on """ 363 | return self.api('closeMarginPosition', {'currencyPair': str(pair)}) 364 | 365 | def marginBuy(self, pair, rate, amount, lendingRate=2): 366 | """ Creates margin buy order at for """ 367 | return self.api('marginBuy', { 368 | 'currencyPair': str(pair), 369 | 'rate': str(rate), 370 | 'amount': str(amount), 371 | 'lendingRate': str(lendingRate) 372 | }) 373 | 374 | def marginSell(self, pair, rate, amount, lendingRate=2): 375 | """ Creates margin sell order at for """ 376 | return self.api('marginSell', { 377 | 'currencyPair': str(pair), 378 | 'rate': str(rate), 379 | 'amount': str(amount), 380 | 'lendingRate': str(lendingRate) 381 | }) 382 | 383 | def buy(self, pair, rate, amount): 384 | """ Creates buy order for at for """ 385 | return self.api('buy', { 386 | 'currencyPair': str(pair), 387 | 'rate': str(rate), 388 | 'amount': str(amount) 389 | }) 390 | 391 | def sell(self, pair, rate, amount): 392 | """ Creates sell order for at for """ 393 | return self.api('sell', { 394 | 'currencyPair': str(pair), 395 | 'rate': str(rate), 396 | 'amount': str(amount) 397 | }) 398 | 399 | def cancelOrder(self, orderId): 400 | """ Cancels order """ 401 | return self.api('cancelOrder', {'orderNumber': str(orderId)}) 402 | 403 | def moveOrder(self, orderId, rate, amount): 404 | """ Moves an order by to for """ 405 | return self.api('moveOrder', { 406 | 'orderNumber': str(orderId), 407 | 'rate': str(rate), 408 | 'amount': str(amount) 409 | }) 410 | 411 | def withdraw(self, coin, amount, address): 412 | """ Withdraws to

""" 413 | return self.api('withdraw', { 414 | 'currency': str(coin), 415 | 'amount': str(amount), 416 | 'address': str(address) 417 | }) 418 | 419 | def returnFeeInfo(self): 420 | """ Returns current trading fees and trailing 30-day volume in BTC """ 421 | return self.api('returnFeeInfo') 422 | 423 | def transferBalance(self, coin, amount, fromac, toac): 424 | """ 425 | Transfers coins between accounts (exchange, margin, lending) 426 | - moves from to 427 | """ 428 | return self.api('transferBalance', { 429 | 'currency': str(coin), 430 | 'amount': str(amount), 431 | 'fromAccount': str(fromac), 432 | 'toAccount': str(toac) 433 | }) 434 | 435 | 436 | class Coach(object): 437 | """ 438 | Coaches the api wrapper, makes sure it doesn't get all hyped up on Mt.Dew 439 | Poloniex default call limit is 6 calls per 1 sec. 440 | """ 441 | def __init__(self, timeFrame=1.0, callLimit=6): 442 | """ 443 | timeFrame = float time in secs [default = 1.0] 444 | callLimit = int max amount of calls per 'timeFrame' [default = 6] 445 | """ 446 | self._timeFrame, self._callLimit = [timeFrame, callLimit] 447 | self._timeBook = [] 448 | 449 | def wait(self): 450 | """ Makes sure our api calls don't go past the api call limit """ 451 | # what time is it? 452 | now = time.time() 453 | # if it's our turn 454 | if len(self._timeBook) is 0 or \ 455 | (now - self._timeBook[-1]) >= self._timeFrame: 456 | # add 'now' to the front of 'timeBook', pushing other times back 457 | self._timeBook.insert(0, now) 458 | logging.info( 459 | "Now: %d Oldest Call: %d Diff: %f sec" % 460 | (now, self._timeBook[-1], now - self._timeBook[-1]) 461 | ) 462 | # 'timeBook' list is longer than 'callLimit'? 463 | if len(self._timeBook) > self._callLimit: 464 | # remove the oldest time 465 | self._timeBook.pop() 466 | else: 467 | logging.info( 468 | "Now: %d Oldest Call: %d Diff: %f sec" % 469 | (now, self._timeBook[-1], now - self._timeBook[-1]) 470 | ) 471 | logging.info( 472 | "Waiting %s sec..." % 473 | str(self._timeFrame-(now - self._timeBook[-1])) 474 | ) 475 | # wait your turn (maxTime - (now - oldest)) = time left to wait 476 | time.sleep(self._timeFrame-(now - self._timeBook[-1])) 477 | # add 'now' to the front of 'timeBook', pushing other times back 478 | self._timeBook.insert(0, time.time()) 479 | # 'timeBook' list is longer than 'callLimit'? 480 | if len(self._timeBook) > self._callLimit: 481 | # remove the oldest time 482 | self._timeBook.pop() 483 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from Arbitrage import Arbitrage 2 | from build_brokers import build_brokers 3 | import config 4 | 5 | brokers = build_brokers(config.TRADE_MODE, config.PAIRS, config.EXCHANGES) 6 | bot = Arbitrage(config, brokers) 7 | # TODO: Loop for the bot to fetch data and make calculations once every couple of seconds 8 | for pair in config.PAIRS: 9 | bot.run_trade(pair) --------------------------------------------------------------------------------