· 6 years ago · Oct 11, 2019, 07:50 AM
1import requests
2import logging
3from concurrent.futures import ThreadPoolExecutor
4import pandas as pd
5import json
6
7
8log = logging.getLogger(__name__)
9
10# --------------------------- constants -----------------------
11
12class BitXAPIError(ValueError):
13 def __init__(self, response):
14 self.url = response.url
15 self.code = response.status_code
16 self.message = response.text
17
18 def __str__(self):
19 return "BitX request %s failed with %d: %s" % (self.url, self.code, self.message)
20
21
22class BitX:
23 def __init__(self, key, secret, options={}):
24 self.options = options
25 self.auth = (key, secret)
26 if 'hostname' in options:
27 self.hostname = options['hostname']
28 else:
29 self.hostname = 'api.mybitx.com'
30 self.port = options['port'] if 'port' in options else 443
31 self.pair = options['pair'] if 'pair' in options else 'XBTZAR'
32 self.ca = options['ca'] if 'ca' in options else None
33 self.timeout = options['timeout'] if 'timeout' in options else 30
34 # Use a Requests session so that we can keep headers and connections
35 # across API requests
36 self._requests_session = requests.Session()
37 self._requests_session.headers.update({
38 'Accept': 'application/json',
39 'Accept-Charset': 'utf-8',
40 'User-Agent': 'py-bitx v' + '1'
41 })
42 self._executor = ThreadPoolExecutor(max_workers=5)
43
44 def close(self):
45 log.info('Asking MultiThreadPool to shutdown')
46 self._executor.shutdown(wait=True)
47 log.info('MultiThreadPool has shutdown')
48
49 def construct_url(self, call):
50 base = self.hostname
51 if self.port != 443:
52 base += ':%d' % (self.port,)
53 return "https://%s/api/1/%s" % (base, call)
54
55 def api_request(self, call, params, kind='auth', http_call='get'):
56 """
57 General API request. Generally, use the convenience functions below
58 :param kind: the type of request to make. 'auth' makes an authenticated call; 'basic' is unauthenticated
59 :param call: the API call to make
60 :param params: a dict of query parameters
61 :return: a json response, a BitXAPIError is thrown if the api returns with an error
62 """
63 url = self.construct_url(call)
64 auth = self.auth if kind == 'auth' else None
65 if http_call == 'get':
66 response = self._requests_session.get(
67 url, params = params, auth = auth, timeout = self.timeout)
68 elif http_call == 'post':
69 response = self._requests_session.post(
70 url, data = params, auth = auth, timeout = self.timeout)
71 else:
72 raise ValueError('Invalid http_call parameter')
73 try:
74 result = response.json()
75 except ValueError:
76 result = {'error': 'No JSON content returned'}
77 if response.status_code != 200 or 'error' in result:
78 raise BitXAPIError(response)
79 else:
80 return result
81
82 def get_ticker(self, kind='auth'):
83 params = {'pair': self.pair}
84 return self.api_request('ticker', params, kind=kind)
85
86 def get_all_tickers(self, kind='auth'):
87 return self.api_request('tickers', None, kind=kind)
88
89 def get_order_book(self, limit=None, kind='auth', since=None):
90 params = {'pair': self.pair}
91 if since != None:
92 params['since'] = since
93 orders = self.api_request('orderbook', params, kind=kind)
94 if limit is not None:
95 orders['bids'] = orders['bids'][:limit]
96 orders['asks'] = orders['asks'][:limit]
97 return orders
98
99 def get_order_book_frame(self, limit=None, kind='auth'):
100 q = self.get_order_book(limit, kind)
101 asks = pd.DataFrame(q['asks'])
102 bids = pd.DataFrame(q['bids'])
103 index = pd.MultiIndex.from_product([('asks', 'bids'),('price', 'volume')])
104 df = pd.DataFrame(pd.concat([asks, bids], axis=1).values, columns=index)
105 return df
106
107 def get_trades(self, limit=None, kind='auth'):
108 params = {'pair': self.pair}
109 trades = self.api_request('trades', params, kind=kind)
110 if limit is not None:
111 trades['trades'] = trades['trades'][:limit]
112 return trades
113
114 def get_trades_frame(self, limit=None, kind='auth'):
115 trades = self.get_trades(limit, kind)
116 df = pd.DataFrame(trades['trades'])
117 df.index = pd.to_datetime(df.timestamp * 1e-3, unit='s')
118 df.drop('timestamp', axis=1, inplace=True)
119 return df
120
121 def get_orders(self, state=None, kind='auth', since=None):
122 """
123 Returns a list of the most recently placed orders. You can specify an optional state='PENDING' parameter to
124 restrict the results to only open orders. You can also specify the market by using the optional pair parameter.
125 The list is truncated after 100 items.
126 :param kind: typically 'auth' if you want this to return anything useful
127 :param state: String optional 'COMPLETE', 'PENDING', or None (default)
128 :return:
129 """
130 params = {'pair': self.pair}
131 if state is not None:
132 params['state'] = state
133 if since is not None:
134 params['since'] = since
135 return self.api_request('listorders', params, kind=kind)
136
137 def get_order(self, order_id):
138 """
139 Get an order by its ID
140 :param order_id: string The order ID
141 :return: dict order details or BitXAPIError raised
142 """
143 return self.api_request('orders/%s' % (order_id,), None)
144
145 def get_orders_frame(self, state=None, kind='auth'):
146 q = self.get_orders(state, kind)
147 tj = json.dumps(q['orders'])
148 df = pd.read_json(tj, convert_dates=['creation_timestamp', 'expiration_timestamp'])
149 df.index = df.creation_timestamp
150 return df
151
152 def create_limit_order(self, order_type, volume, price):
153 """
154 Create a new limit order
155 :param order_type: 'buy' or 'sell'
156 :param volume: the volume, in BTC
157 :param price: the ZAR price per bitcoin
158 :return: the order id
159 """
160 data = {
161 'pair': self.pair,
162 'type': 'BID' if order_type == 'buy' else 'ASK',
163 'volume': str(volume),
164 'price': str(price)
165
166 }
167 result = self.api_request('postorder', params=data, http_call='post')
168 return result
169
170 def stop_order(self, order_id):
171 """
172 Create a new limit order
173 :param order_id: The order ID
174 :return: a success flag
175 """
176 data = {
177 'order_id': order_id,
178 }
179 return self.api_request('stoporder', params=data, http_call='post')
180
181 def stop_all_orders(self):
182 """
183 Stops all pending orders, both sell and buy
184 :return: dict of Boolean -- whether request succeeded or not for each order_id that was pending
185 """
186 pending = self.get_orders('PENDING')['orders']
187 ids = [order['order_id'] for order in pending]
188 result = {}
189 for order_id in ids:
190 status = self.stop_order(order_id)
191 result[order_id] = status['success']
192 return result
193
194
195 def get_funding_address(self, asset):
196 """
197 Returns the default receive address associated with your account and the amount received via the address. You
198 can specify an optional address parameter to return information for a non-default receive address. In the
199 response, total_received is the total confirmed Bitcoin amount received excluding unconfirmed transactions.
200 total_unconfirmed is the total sum of unconfirmed receive transactions.
201 :param asset: For now, only XBT is valid
202 :return: dict
203 """
204 return self.api_request('funding_address', {'asset': asset})
205
206 def get_withdrawals_status(self, wid=None):
207 """
208 :param wid: String. Optional withdrawal id. None queries for all ids
209 :return:
210 """
211 call = 'withdrawals'
212 if wid is not None:
213 call += '/%s' % (wid,)
214 return self.api_request(call, None)
215
216 def get_balance(self):
217 return self.api_request('balance', None)
218
219 def get_transactions(self, account_id, min_row=None, max_row=None):
220 params = {}
221 if min_row is not None:
222 params['min_row'] = min_row
223 if max_row is not None:
224 params['max_row'] = max_row
225 return self.api_request('accounts/%s/transactions' % (account_id,), params)
226
227 def get_transactions_frame(self, account_id, min_row=None, max_row=None):
228 tx = self.get_transactions(account_id, min_row, max_row)['transactions']
229 df = pd.DataFrame(tx)
230 df.index = pd.to_datetime(df.timestamp, unit='ms')
231 df.drop('timestamp', axis=1, inplace=True)
232 return df
233
234 def get_pending_transactions(self, account_id):
235 return self.api_request('accounts/%s/pending' % (account_id,), None)