1 | #! /usr/bin/env python |
||
2 | """API Wrapper for Bitcoin.de Trading API.""" |
||
3 | |||
4 | import requests |
||
5 | import time |
||
6 | import json |
||
7 | import hmac |
||
8 | import hashlib |
||
9 | import logging |
||
10 | import codecs |
||
11 | import decimal |
||
12 | import inspect |
||
13 | import urllib |
||
14 | |||
15 | from future.standard_library import install_aliases |
||
16 | install_aliases() |
||
17 | |||
18 | from urllib.parse import urlencode |
||
19 | |||
20 | logging.basicConfig() |
||
21 | log = logging.getLogger(__name__) |
||
22 | requests_log = logging.getLogger("requests.packages.urllib3") |
||
23 | requests_log.propagate = True |
||
24 | |||
25 | __version__ = '2.3' |
||
26 | |||
27 | # disable unsecure SSL warning |
||
28 | requests.packages.urllib3.disable_warnings() |
||
29 | |||
30 | class ParameterBuilder(object): |
||
31 | '''To verify given parameters for API.''' |
||
32 | def __init__(self, avail_params, given_params, uri): |
||
33 | self.verify_keys_and_values(avail_params, given_params) |
||
34 | self.params = given_params |
||
35 | self.create_url(uri) |
||
36 | |||
37 | def verify_keys_and_values(self, avail_params, given_params): |
||
38 | for k, v in given_params.items(): |
||
39 | if k not in avail_params: |
||
40 | list_string = ', '.join(avail_params) |
||
41 | raise KeyError("{} is not any of {}".format(k, list_string)) |
||
42 | if k == 'trading_pair': |
||
43 | self.error_on_invalid_value(v, self.TRADING_PAIRS) |
||
44 | elif k == 'type': |
||
45 | self.error_on_invalid_value(v, self.ORDER_TYPES) |
||
46 | elif k == 'currency': |
||
47 | self.error_on_invalid_value(v, self.CURRENCIES) |
||
48 | elif k == 'seat_of_bank': |
||
49 | self.error_on_invalid_value(v, self.BANK_SEATS) |
||
50 | elif k in ['min_trust_level', 'trust_level']: |
||
51 | self.error_on_invalid_value(v, self.TRUST_LEVELS) |
||
52 | elif k == 'payment_option': |
||
53 | self.error_on_invalid_value(v, self.PAYMENT_OPTIONS) |
||
54 | elif k == 'state': |
||
55 | caller = inspect.stack()[2][3] |
||
56 | if caller in ["showMyOrders", "showMyOrderDetails"]: |
||
57 | self.error_on_invalid_value(v, self.ORDER_STATES) |
||
58 | elif caller in ["showMyTrades", "showMyTradesDetails"]: |
||
59 | self.error_on_invalid_value(v, self.TRADE_STATES) |
||
60 | |||
61 | def error_on_invalid_value(self, value, list): |
||
62 | if value not in list: |
||
63 | list_string = ', '.join(str(x) for x in list) |
||
64 | raise ValueError("{} is not any of {}".format(value, list_string)) |
||
65 | |||
66 | def create_url(self, uri): |
||
67 | if self.params: |
||
68 | self.encoded_string = urlencode(self.params) |
||
69 | self.url = uri + '?' + self.encoded_string |
||
70 | else: |
||
71 | self.encoded_string = '' |
||
72 | self.url = uri |
||
73 | |||
74 | |||
75 | TRADING_PAIRS = ['btceur', 'bcheur', 'etheur', 'btgeur', 'bsveur'] |
||
76 | ORDER_TYPES = ['buy', 'sell'] |
||
77 | CURRENCIES = ['btc', 'bch', 'eth', 'btg', 'bsv'] |
||
78 | BANK_SEATS = ['AT', 'BE', 'BG', 'CH', 'CY', 'CZ', |
||
79 | 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', |
||
80 | 'GB', 'GR', 'HR', 'HU', 'IE', 'IS', |
||
81 | 'IT', 'LI', 'LT', 'LU', 'LV', 'MT', |
||
82 | 'MQ', 'NL', 'NO', 'PL', 'PT', 'RO', |
||
83 | 'SE', 'SI', 'SK'] |
||
84 | TRUST_LEVELS = ['bronze', 'silver', 'gold', 'platin'] |
||
85 | TRADE_STATES = [-1, 0, 1] |
||
86 | ORDER_STATES = [-2, -1, 0] |
||
87 | PAYMENT_OPTIONS = [1, 2, 3] |
||
88 | TRADE_TYPES = ['all', 'buy', 'sell', 'inpayment', |
||
89 | 'payout', 'affiliate', 'welcome_btc', |
||
90 | 'buy_yubikey', 'buy_goldshop', |
||
91 | 'buy_diamondshop', 'kickback', |
||
92 | 'outgoing_fee_voluntary'] |
||
93 | |||
94 | def HandleRequestsException(e): |
||
95 | """Handle Exception from request.""" |
||
96 | log.warning(e) |
||
97 | |||
98 | |||
99 | def HandleAPIErrors(r): |
||
100 | """To handle Errors from BTCDE API.""" |
||
101 | valid_status_codes = [200, 201, 204] |
||
102 | if r.status_code not in valid_status_codes: |
||
103 | content = r.json() |
||
104 | errors = content.get('errors') |
||
105 | log.warning('API Error Code: {}'.format(str(errors[0]['code']))) |
||
106 | log.warning('API Error Message: {}'.format(errors[0]['message'])) |
||
107 | log.warning('API Error URL: {}'.format(r.url)) |
||
108 | return False |
||
109 | else: |
||
110 | return True |
||
111 | |||
112 | |||
113 | class Connection(object): |
||
114 | """To provide connection credentials to the trading API""" |
||
115 | def __init__(self, api_key, api_secret): |
||
116 | self.api_key = api_key |
||
117 | self.api_secret = api_secret |
||
118 | # set initial self.nonce |
||
119 | self.nonce = int(time.time() * 1000000) |
||
120 | # Bitcoin.de API URI |
||
121 | self.apihost = 'https://api.bitcoin.de' |
||
122 | self.apiversion = 'v2' |
||
123 | self.orderuri = self.apihost + '/' + self.apiversion + '/' + 'orders' |
||
124 | self.tradeuri = self.apihost + '/' + self.apiversion + '/' + 'trades' |
||
125 | self.accounturi = self.apihost + '/' + self.apiversion + '/' + 'account' |
||
126 | |||
127 | def build_hmac_sign(self, md5string, method, url): |
||
128 | hmac_data = '{method}#{url}#{key}#{nonce}#{md5}'\ |
||
129 | .format(method=method, url=url, |
||
130 | key=self.api_key, nonce=str(self.nonce), |
||
131 | md5=md5string) |
||
132 | hmac_signed = hmac.new(bytearray(self.api_secret.encode()), msg=hmac_data.encode(), digestmod=hashlib.sha256).hexdigest() |
||
133 | return hmac_signed |
||
134 | |||
135 | def set_header(self, url, method, encoded_string): |
||
136 | # raise self.nonce before using |
||
137 | self.nonce = int(time.time() * 1000000) |
||
138 | if method == 'POST': |
||
139 | md5_encoded_query_string = hashlib.md5(encoded_string.encode()).hexdigest() |
||
140 | else: |
||
141 | md5_encoded_query_string = hashlib.md5(b'').hexdigest() |
||
142 | hmac_signed = self.build_hmac_sign(md5_encoded_query_string, |
||
143 | method, url) |
||
144 | # set header |
||
145 | header = {'content-type': |
||
146 | 'application/x-www-form-urlencoded; charset=utf-8', |
||
147 | 'X-API-KEY': self.api_key, |
||
148 | 'X-API-NONCE': str(self.nonce), |
||
149 | 'X-API-SIGNATURE': hmac_signed } |
||
150 | return header |
||
151 | |||
152 | def send_request(self, url, method, header, encoded_string): |
||
153 | if method == 'GET': |
||
154 | r = requests.get(url, headers=(header), |
||
155 | stream=True, verify=False) |
||
156 | elif method == 'POST': |
||
157 | r = requests.post(url, headers=(header), data=encoded_string, |
||
158 | stream=True, verify=False) |
||
159 | elif method == 'DELETE': |
||
160 | r = requests.delete(url, headers=(header), |
||
161 | stream=True, verify=False) |
||
162 | return r |
||
0 ignored issues
–
show
introduced
by
![]() |
|||
163 | |||
164 | def APIConnect(self, method, params): |
||
165 | """Transform Parameters to URL""" |
||
166 | header = self.set_header(params.url, method, |
||
167 | params.encoded_string) |
||
168 | log.debug('Set Header: {}'.format(header)) |
||
169 | try: |
||
170 | r = self.send_request(params.url, method, header, |
||
171 | params.encoded_string) |
||
172 | # Handle API Errors |
||
173 | if HandleAPIErrors(r): |
||
174 | # get results |
||
175 | result = r.json(parse_float=decimal.Decimal) |
||
176 | else: |
||
177 | result = {} |
||
178 | except requests.exceptions.RequestException as e: |
||
179 | HandleRequestsException(e) |
||
180 | result = {} |
||
181 | return result |
||
182 | |||
183 | def showOrderbook(self, order_type, trading_pair, **args): |
||
184 | """Search Orderbook for offers.""" |
||
185 | params = {'type': order_type, |
||
186 | 'trading_pair': trading_pair} |
||
187 | params.update(args) |
||
188 | avail_params = ['type', 'trading_pair', 'amount', 'price', |
||
189 | 'order_requirements_fullfilled', |
||
190 | 'only_kyc_full', 'only_express_orders', |
||
191 | 'only_same_bankgroup', 'only_same_bic', |
||
192 | 'seat_of_bank'] |
||
193 | p = ParameterBuilder(avail_params, params, self.orderuri) |
||
194 | return self.APIConnect('GET', p) |
||
195 | |||
196 | |||
197 | def createOrder(self, order_type, trading_pair, max_amount, price, **args): |
||
198 | """Create a new Order.""" |
||
199 | # Build parameters |
||
200 | params = {'type': order_type, |
||
201 | 'trading_pair': trading_pair, |
||
202 | 'max_amount': max_amount, |
||
203 | 'price': price} |
||
204 | params.update(args) |
||
205 | avail_params = ['type', 'trading_pair', 'max_amount', 'price', |
||
206 | 'min_amount', 'new_order_for_remaining_amount', |
||
207 | 'min_trust_level', 'only_kyc_full', 'payment_option', |
||
208 | 'seat_of_bank'] |
||
209 | p = ParameterBuilder(avail_params, params, self.orderuri) |
||
210 | return self.APIConnect('POST', p) |
||
211 | |||
212 | |||
213 | def deleteOrder(self, order_id, trading_pair): |
||
214 | """Delete an Order.""" |
||
215 | # Build parameters |
||
216 | params = {'order_id': order_id, |
||
217 | 'trading_pair': trading_pair} |
||
218 | avail_params = ['order_id', 'trading_pair'] |
||
219 | newuri = self.orderuri + "/" + order_id + "/" + trading_pair |
||
220 | p = ParameterBuilder(avail_params, params, newuri) |
||
221 | p.encoded_string = '' |
||
222 | p.url = newuri |
||
223 | return self.APIConnect('DELETE', p) |
||
224 | |||
225 | |||
226 | def showMyOrders(self, **args): |
||
227 | """Query and Filter own Orders.""" |
||
228 | # Build parameters |
||
229 | params = args |
||
230 | avail_params = ['type', 'trading_pair', 'state', |
||
231 | 'date_start', 'date_end', 'page'] |
||
232 | newuri = self.orderuri + '/my_own' |
||
233 | p = ParameterBuilder(avail_params, params, newuri) |
||
234 | return self.APIConnect('GET', p) |
||
235 | |||
236 | |||
237 | def showMyOrderDetails(self, order_id): |
||
238 | """Details to an own Order.""" |
||
239 | newuri = self.orderuri + '/' + order_id |
||
240 | p = ParameterBuilder({}, {}, newuri) |
||
241 | return self.APIConnect('GET', p) |
||
242 | |||
243 | |||
244 | def executeTrade(self, order_id, order_type, trading_pair, amount): |
||
245 | """Buy/Sell on a specific Order.""" |
||
246 | newuri = self.tradeuri + '/' + order_id |
||
247 | params = {'order_id': order_id, |
||
248 | 'type': order_type, |
||
249 | 'trading_pair': trading_pair, |
||
250 | 'amount': amount} |
||
251 | avail_params = ['order_id', 'type', 'trading_pair', |
||
252 | 'amount'] |
||
253 | p = ParameterBuilder(avail_params, params, newuri) |
||
254 | return self.APIConnect('POST', p) |
||
255 | |||
256 | |||
257 | def showMyTrades(self, **args): |
||
258 | """Query and Filter on past Trades.""" |
||
259 | # Build parameters |
||
260 | params = args |
||
261 | avail_params = ['type', 'trading_pair', 'state', |
||
262 | 'date_start', 'date_end', 'page'] |
||
263 | p = ParameterBuilder(avail_params, params, self.tradeuri) |
||
264 | return self.APIConnect('GET', p) |
||
265 | |||
266 | |||
267 | def showMyTradeDetails(self, trade_id): |
||
268 | """Details to a specific Trade.""" |
||
269 | newuri = self.tradeuri + '/' + trade_id |
||
270 | params = {} |
||
271 | p = ParameterBuilder({}, {}, newuri) |
||
272 | return self.APIConnect('GET', p) |
||
273 | |||
274 | |||
275 | def showAccountInfo(self): |
||
276 | """Query on Account Infos.""" |
||
277 | p = ParameterBuilder({}, {}, self.accounturi) |
||
278 | return self.APIConnect('GET', p) |
||
279 | |||
280 | |||
281 | def showOrderbookCompact(self, trading_pair): |
||
282 | """Bids and Asks in compact format.""" |
||
283 | params = {'trading_pair': trading_pair} |
||
284 | # Build parameters |
||
285 | avail_params = ['trading_pair'] |
||
286 | p = ParameterBuilder(avail_params, params, |
||
287 | self.orderuri + '/compact') |
||
288 | return self.APIConnect('GET', p) |
||
289 | |||
290 | |||
291 | def showPublicTradeHistory(self, trading_pair, **args): |
||
292 | """All successful trades of the las 7 days.""" |
||
293 | params = {'trading_pair': trading_pair} |
||
294 | params.update(args) |
||
295 | avail_params = ['trading_pair', 'since_tid'] |
||
296 | p = ParameterBuilder(avail_params, params, |
||
297 | self.tradeuri + '/history') |
||
298 | return self.APIConnect('GET', p) |
||
299 | |||
300 | |||
301 | def showRates(self, trading_pair): |
||
302 | """Query of the average rate last 3 and 12 hours.""" |
||
303 | newuri = self.apihost + '/' + self.apiversion + '/rates' |
||
304 | params = {'trading_pair': trading_pair} |
||
305 | avail_params = ['trading_pair'] |
||
306 | p = ParameterBuilder(avail_params, params, newuri) |
||
307 | return self.APIConnect('GET', p) |
||
308 | |||
309 | |||
310 | def showAccountLedger(self, currency, **args): |
||
311 | """Query on Account statement.""" |
||
312 | params = {'currency': currency} |
||
313 | params.update(args) |
||
314 | avail_params = ['currency', 'type', |
||
315 | 'datetime_start', 'datetime_end', 'page'] |
||
316 | p = ParameterBuilder(avail_params, params, |
||
317 | self.accounturi + '/ledger') |
||
318 | return self.APIConnect('GET', p) |
||
319 |