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