forked from kmadac/bitstamp-python-client
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclient.py
More file actions
executable file
·615 lines (523 loc) · 20.6 KB
/
client.py
File metadata and controls
executable file
·615 lines (523 loc) · 20.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
from functools import wraps
import hmac
import hashlib
import time
import warnings
import logging
import requests
logger = logging.getLogger(__name__)
class BitstampError(Exception):
pass
class TransRange(object):
"""
Enum like object used in transaction method to specify time range
from which to get list of transactions
"""
HOUR = 'hour'
MINUTE = 'minute'
DAY = 'day'
class BaseClient(object):
"""
A base class for the API Client methods that handles interaction with
the requests library.
"""
api_url = {1: 'https://www.bitstamp.net/api/',
2: 'https://www.bitstamp.net/api/v2/'}
exception_on_error = True
def __init__(self, proxydict=None, *args, **kwargs):
self.proxydict = proxydict
def _get(self, *args, **kwargs):
"""
Make a GET request.
"""
return self._request(requests.get, *args, **kwargs)
def _post(self, *args, **kwargs):
"""
Make a POST request.
"""
data = self._default_data()
data.update(kwargs.get('data') or {})
kwargs['data'] = data
return self._request(requests.post, *args, **kwargs)
def _default_data(self):
"""
Default data for a POST request.
"""
return {}
def _construct_url(self, url, base, quote):
"""
Adds the orderbook to the url if base and quote are specified.
"""
if not base and not quote:
return url
else:
url = url + base.lower() + quote.lower() + "/"
return url
def _request(self, func, url, version=1, *args, **kwargs):
"""
Make a generic request, adding in any proxy defined by the instance.
Raises a ``requests.HTTPError`` if the response status isn't 200, and
raises a :class:`BitstampError` if the response contains a json encoded
error message.
"""
return_json = kwargs.pop('return_json', False)
url = self.api_url[version] + url
logger.debug("Request URL: " + url)
if 'data' in kwargs and 'nonce' in kwargs['data']:
logger.debug("Request nonce: " + str(kwargs['data']['nonce']))
response = func(url, *args, **kwargs)
logger.debug("Response Code {} and Reason {}".format(response.status_code, response.reason))
logger.debug("Response Text {}".format(response.text))
if 'proxies' not in kwargs:
kwargs['proxies'] = self.proxydict
# Check for error, raising an exception if appropriate.
response.raise_for_status()
try:
json_response = response.json()
except ValueError:
json_response = None
if isinstance(json_response, dict):
error = json_response.get('error')
if error:
raise BitstampError(error)
elif json_response.get('status') == "error":
raise BitstampError(json_response.get('reason'))
if return_json:
if json_response is None:
raise BitstampError(
"Could not decode json for: " + response.text)
return json_response
return response
class Public(BaseClient):
def ticker(self, base="btc", quote="usd"):
"""
Returns dictionary.
"""
url = self._construct_url("ticker/", base, quote)
return self._get(url, return_json=True, version=2)
def ticker_hour(self, base="btc", quote="usd"):
"""
Returns dictionary of the average ticker of the past hour.
"""
url = self._construct_url("ticker_hour/", base, quote)
return self._get(url, return_json=True, version=2)
def order_book(self, group=True, base="btc", quote="usd"):
"""
Returns dictionary with "bids" and "asks".
Each is a list of open orders and each order is represented as a list
of price and amount.
"""
params = {'group': group}
url = self._construct_url("order_book/", base, quote)
return self._get(url, params=params, return_json=True, version=2)
def transactions(self, time=TransRange.HOUR, base="btc", quote="usd"):
"""
Returns transactions for the last 'timedelta' seconds.
Parameter time is specified by one of two values of TransRange class.
"""
params = {'time': time}
url = self._construct_url("transactions/", base, quote)
return self._get(url, params=params, return_json=True, version=2)
def conversion_rate_usd_eur(self):
"""
Returns simple dictionary::
{'buy': 'buy conversion rate', 'sell': 'sell conversion rate'}
"""
return self._get("eur_usd/", return_json=True, version=1)
def trading_pairs_info(self):
"""
Returns list of dictionaries specifying details of each available trading pair::
{
'description':'Litecoin / U.S. dollar',
'name':'LTC/USD',
'url_symbol':'ltcusd',
'trading':'Enabled',
'minimum_order':'5.0 USD',
'counter_decimals':2,
'base_decimals':8
},
"""
return self._get("trading-pairs-info/", return_json=True, version=2)
class Trading(Public):
def __init__(self, username, key, secret, *args, **kwargs):
"""
Stores the username, key, and secret which is used when making POST
requests to Bitstamp.
"""
super(Trading, self).__init__(
username=username, key=key, secret=secret, *args, **kwargs)
self.username = username
self.key = key
self.secret = secret
def get_nonce(self):
"""
Get a unique nonce for the bitstamp API.
This integer must always be increasing, so use the current unix time.
Every time this variable is requested, it automatically increments to
allow for more than one API request per second.
This isn't a thread-safe function however, so you should only rely on a
single thread if you have a high level of concurrent API requests in
your application.
"""
nonce = getattr(self, '_nonce', 0)
if nonce:
nonce += 1
# If the unix time is greater though, use that instead (helps low
# concurrency multi-threaded apps always call with the largest nonce).
self._nonce = max(int(time.time()), nonce)
return self._nonce
def _default_data(self, *args, **kwargs):
"""
Generate a one-time signature and other data required to send a secure
POST request to the Bitstamp API.
"""
data = super(Trading, self)._default_data(*args, **kwargs)
data['key'] = self.key
nonce = self.get_nonce()
msg = str(nonce) + self.username + self.key
signature = hmac.new(
self.secret.encode('utf-8'), msg=msg.encode('utf-8'),
digestmod=hashlib.sha256).hexdigest().upper()
data['signature'] = signature
data['nonce'] = nonce
return data
def _expect_true(self, response):
"""
A shortcut that raises a :class:`BitstampError` if the response didn't
just contain the text 'true'.
"""
if response.text == u'true':
return True
raise BitstampError("Unexpected response")
def account_balance(self, base="btc", quote="usd"):
"""
Returns dictionary::
{u'btc_reserved': u'0',
u'fee': u'0.5000',
u'btc_available': u'2.30856098',
u'usd_reserved': u'0',
u'btc_balance': u'2.30856098',
u'usd_balance': u'114.64',
u'usd_available': u'114.64',
---If base and quote were specified:
u'fee': u'',
---If base and quote were not specified:
u'btcusd_fee': u'0.25',
u'btceur_fee': u'0.25',
u'eurusd_fee': u'0.20',
}
There could be reasons to set base and quote to None (or False),
because the result then will contain the fees for all currency pairs
For backwards compatibility this can not be the default however.
"""
url = self._construct_url("balance/", base, quote)
return self._post(url, return_json=True, version=2)
def user_transactions(self, offset=0, limit=1000, descending=True,
base=None, quote=None, since_timestamp=None, since_id=None):
"""
Returns descending list of transactions. Every transaction (dictionary)
contains::
{u'usd': u'-39.25',
u'datetime': u'2013-03-26 18:49:13',
u'fee': u'0.20',
u'btc': u'0.50000000',
u'type': 2,
u'id': 213642}
Instead of the keys btc and usd, it can contain other currency codes
"""
data = {
'offset': offset,
'limit': limit,
'sort': 'desc' if descending else 'asc',
}
if since_timestamp is not None:
data.update({'since_timestamp': since_timestamp})
if since_id is not None:
data.update({'since_id': since_id})
url = self._construct_url("user_transactions/", base, quote)
return self._post(url, data=data, return_json=True, version=2)
def crypto_transactions(self, offset=0, limit=1000):
"""
Returns list of crypto transactions. Every transaction (dictionary)
contains::
{u'currency' : u'LTC'
u'destinationAddress': u'Destination Address'
u'txid': u'Transaction Hash'
u'amount' u'1.0'
u'datetime': int(timestamp)}
Instead of the keys btc and usd, it can contain other currency codes
"""
data = {
'offset': offset,
'limit': limit,
}
url = "crypto-transactions/"
return self._post(url, data=data, return_json=True, version=2)
def open_orders(self, base="btc", quote="usd"):
"""
Returns JSON list of open orders. Each order is represented as a
dictionary.
"""
url = self._construct_url("open_orders/", base, quote)
return self._post(url, return_json=True, version=2)
def all_open_orders(self):
"""
Returns JSON list of open orders of all currency pairs.
Each order is represented as a dictionary.
"""
return self._post('open_orders/all/', return_json=True, version=2)
def order_status(self, order_id):
"""
Returns dictionary.
- status: 'Finished'
or 'In Queue'
or 'Open'
- transactions: list of transactions
Each transaction is a dictionary with the following keys:
btc, usd, price, type, fee, datetime, tid
or btc, eur, ....
or eur, usd, ....
"""
data = {'id': order_id}
return self._post("order_status/", data=data, return_json=True,
version=1)
def cancel_order(self, order_id, version=1):
"""
Cancel the order specified by order_id.
Version 1 (default for backwards compatibility reasons):
Returns True if order was successfully canceled, otherwise
raise a BitstampError.
Version 2:
Returns dictionary of the canceled order, containing the keys:
id, type, price, amount
"""
data = {'id': order_id}
return self._post("cancel_order/", data=data, return_json=True,
version=version)
def cancel_all_orders(self):
"""
Cancel all open orders.
Returns True if it was successful, otherwise raises a
BitstampError.
"""
return self._post("cancel_all_orders/", return_json=True, version=1)
def buy_limit_order(self, amount, price, base="btc", quote="usd", limit_price=None, ioc_order=False):
"""
Order to buy amount of bitcoins for specified price.
"""
data = {'amount': amount, 'price': price}
if limit_price is not None:
data['limit_price'] = limit_price
if ioc_order is True:
data['ioc_order'] = True
url = self._construct_url("buy/", base, quote)
return self._post(url, data=data, return_json=True, version=2)
def buy_market_order(self, amount, base="btc", quote="usd"):
"""
Order to buy amount of bitcoins for market price.
"""
data = {'amount': amount}
url = self._construct_url("buy/market/", base, quote)
return self._post(url, data=data, return_json=True, version=2)
def sell_limit_order(self, amount, price, base="btc", quote="usd", limit_price=None, ioc_order=False):
"""
Order to sell amount of bitcoins for specified price.
"""
data = {'amount': amount, 'price': price}
if limit_price is not None:
data['limit_price'] = limit_price
if ioc_order is True:
data['ioc_order'] = True
url = self._construct_url("sell/", base, quote)
return self._post(url, data=data, return_json=True, version=2)
def sell_market_order(self, amount, base="btc", quote="usd"):
"""
Order to sell amount of bitcoins for market price.
"""
data = {'amount': amount}
url = self._construct_url("sell/market/", base, quote)
return self._post(url, data=data, return_json=True, version=2)
def check_bitstamp_code(self, code):
"""
Returns JSON dictionary containing USD and BTC amount included in given
bitstamp code.
"""
data = {'code': code}
return self._post("check_code/", data=data, return_json=True,
version=1)
def redeem_bitstamp_code(self, code):
"""
Returns JSON dictionary containing USD and BTC amount added to user's
account.
"""
data = {'code': code}
return self._post("redeem_code/", data=data, return_json=True,
version=1)
def withdrawal_requests(self, timedelta = 86400):
"""
Returns list of withdrawal requests.
Each request is represented as a dictionary.
By default, the last 24 hours (86400 seconds) are returned.
"""
data = {'timedelta': timedelta}
return self._post("withdrawal_requests/", return_json=True, version=1, data=data)
def bitcoin_withdrawal(self, amount, address):
"""
Send bitcoins to another bitcoin wallet specified by address.
"""
data = {'amount': amount, 'address': address}
return self._post("bitcoin_withdrawal/", data=data, return_json=True,
version=1)
def bitcoin_deposit_address(self):
"""
Returns bitcoin deposit address as unicode string
"""
return self._post("bitcoin_deposit_address/", return_json=True,
version=1)
def unconfirmed_bitcoin_deposits(self):
"""
Returns JSON list of unconfirmed bitcoin transactions.
Each transaction is represented as dictionary:
amount
bitcoin amount
address
deposit address used
confirmations
number of confirmations
"""
return self._post("unconfirmed_btc/", return_json=True, version=1)
def litecoin_withdrawal(self, amount, address):
"""
Send litecoins to another litecoin wallet specified by address.
"""
data = {'amount': amount, 'address': address}
return self._post("ltc_withdrawal/", data=data, return_json=True,
version=2)
def litecoin_deposit_address(self):
"""
Returns litecoin deposit address as unicode string
"""
return self._post("ltc_address/", return_json=True,
version=2)
def ethereum_withdrawal(self, amount, address):
"""
Send ethers to another ether wallet specified by address.
"""
data = {'amount': amount, 'address': address}
return self._post("eth_withdrawal/", data=data, return_json=True,
version=2)
def ethereum_deposit_address(self):
"""
Returns ethereum deposit address as unicode string
"""
return self._post("eth_address/", return_json=True,
version=2)
def ripple_withdrawal(self, amount, address, currency):
"""
Returns true if successful.
"""
data = {'amount': amount, 'address': address, 'currency': currency}
response = self._post("ripple_withdrawal/", data=data,
return_json=True)
return self._expect_true(response)
def ripple_deposit_address(self):
"""
Returns ripple deposit address as unicode string.
"""
return self._post("ripple_address/", version=1, return_json=True)[
"address"]
def xrp_withdrawal(self, amount, address, destination_tag=None):
"""
Sends xrps to another xrp wallet specified by address. Returns withdrawal id.
"""
data = {'amount': amount, 'address': address}
if destination_tag:
data['destination_tag'] = destination_tag
return self._post("xrp_withdrawal/", data=data, return_json=True,
version=2)["id"]
def xrp_deposit_address(self):
"""
Returns ripple deposit address and destination tag as dictionary.
Example: {u'destination_tag': 53965834, u'address': u'rDsbeamaa4FFwbQTJp9Rs84Q56vCiWCaBx'}
"""
return self._post("xrp_address/", version=2, return_json=True)
def bch_withdrawal(self, amount, address):
"""
Send bitcoin cash to another bitcoin cash wallet specified by address.
"""
data = {'amount': amount, 'address': address}
return self._post("bch_withdrawal/", data=data, return_json=True,
version=2)
def bch_deposit_address(self):
"""
Returns bitcoin cash deposit address as unicode string
"""
return self._post("bch_address/", return_json=True,
version=2)
def transfer_to_main(self, amount, currency, subaccount=None):
"""
Returns dictionary with status.
subaccount has to be the numerical id of the subaccount, not the name
"""
data = {'amount': amount,
'currency': currency,}
if subaccount is not None:
data['subAccount'] = subaccount
return self._post("transfer-to-main/", data=data, return_json=True,
version=2)
def transfer_from_main(self, amount, currency, subaccount):
"""
Returns dictionary with status.
subaccount has to be the numerical id of the subaccount, not the name
"""
data = {'amount': amount,
'currency': currency,
'subAccount': subaccount,}
return self._post("transfer-from-main/", data=data, return_json=True,
version=2)
# Backwards compatibility
class BackwardsCompat(object):
"""
Version 1 used lower case class names that didn't raise an exception when
Bitstamp returned a response indicating an error had occured.
Instead, it returned a tuple containing ``(False, 'The error message')``.
"""
wrapped_class = None
def __init__(self, *args, **kwargs):
"""
Instantiate the wrapped class.
"""
self.wrapped = self.wrapped_class(*args, **kwargs)
class_name = self.__class__.__name__
warnings.warn(
"Use the {} class rather than the deprecated {} one".format(
class_name.title(), class_name),
DeprecationWarning, stacklevel=2)
def __getattr__(self, name):
"""
Return the wrapped attribute. If it's a callable then return the error
tuple when appropriate.
"""
attr = getattr(self.wrapped, name)
if not callable(attr):
return attr
@wraps(attr)
def wrapped_callable(*args, **kwargs):
"""
Catch ``BitstampError`` and replace with the tuple error pair.
"""
try:
return attr(*args, **kwargs)
except BitstampError as e:
return False, e.args[0]
return wrapped_callable
class public(BackwardsCompat):
"""
Deprecated version 1 client. Use :class:`Public` instead.
"""
wrapped_class = Public
class trading(BackwardsCompat):
"""
Deprecated version 1 client. Use :class:`Trading` instead.
"""
wrapped_class = Trading