Completed
Push — master ( a48a20...2f6ef6 )
by Andreas
8s
created

Connection.showAccountLedger()   A

Complexity

Conditions 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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