Skip to content

Commit 38dc133

Browse files
committed
added getRemainingLimit()
1 parent 1d0c95d commit 38dc133

3 files changed

Lines changed: 85 additions & 19 deletions

File tree

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ This is the python wrapper for the Bitvavo API. This project can be used to buil
4545
pip install python-bitvavo-api
4646
```
4747

48+
## Rate Limiting
49+
50+
Bitvavo uses a weight based rate limiting system, with an allowed limit of 1000 per IP or API key each minute. Please inspect each endpoint in the [documentation](https://docs.bitvavo.com/) to see the weight. Failure to respect the rate limit will result in an IP or API key ban.
51+
Since the remaining limit is returned in the header on each REST request, the remaining limit is tracked locally and can be requested through:
52+
```
53+
limit = bitvavo.getRemainingLimit()
54+
```
55+
The websocket functions however do not return a remaining limit, therefore the limit is only updated locally once a ban has been issued.
56+
4857
## REST requests
4958

5059
The general convention used in all functions (both REST and websockets), is that all optional parameters are passed as an dictionary, while required parameters are passed as separate values. Only when [placing orders](https://github.com/bitvavo/python-bitvavo-api#place-order) some of the optional parameters are required, since a limit order requires more information than a market order. The returned responses are all converted to a dictionary as well, such that `response['<key>'] = '<value>'`.
@@ -1060,7 +1069,7 @@ All requests which can be done through REST requests can also be performed over
10601069

10611070
### Getting started
10621071

1063-
The websocket object should be intialised through the `newWebsocket()` function. After which a callback for the errors should be set. After this any desired function can be called. Finally the main thread should be kept alive for as long as you want the socket to stay open. This can be achieved through a simple `while(True)` loop.
1072+
The websocket object should be intialised through the `newWebsocket()` function. After which a callback for the errors should be set. After this any desired function can be called. Finally the main thread should be kept alive for as long as you want the socket to stay open. This can be achieved through a simple `while()` loop, where the remaining limit is checked. This is in case a ban has been issued, otherwise the websocket object will keep trying to reconnect, while our servers keep closing the connection.
10641073

10651074
```python
10661075
def errorCallback(error):
@@ -1075,9 +1084,11 @@ websocket.setErrorCallback(errorCallback)
10751084
# Call functions here, like:
10761085
# websocket.time(ownCallback)
10771086

1087+
limit = bitvavo.getRemainingLimit()
10781088
try:
1079-
while(True):
1080-
time.sleep(2)
1089+
while(limit > 0):
1090+
time.sleep(0.5)
1091+
limit = bitvavo.getRemainingLimit()
10811092
except KeyboardInterrupt:
10821093
websocket.closeSocket()
10831094
```

pythonBitvavoApi/bitvavo.py

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,25 @@ def processLocalBook(ws, message):
8989

9090
ws.callbacks['subscriptionBookUser'][market](ws.localBook[market])
9191

92+
class rateLimitThread (threading.Thread):
93+
def __init__(self, reset, bitvavo):
94+
self.timeToWait = reset
95+
self.bitvavo = bitvavo
96+
threading.Thread.__init__(self)
97+
98+
def waitForReset(self, waitTime):
99+
time.sleep(waitTime)
100+
if (time.time() < self.bitvavo.rateLimitReset):
101+
self.bitvavo.rateLimitRemaining = 1000
102+
debugToConsole('Ban should have been lifted, resetting rate limit to 1000.')
103+
else:
104+
timeToWait = (self.bitvavo.rateLimitReset / 1000) - time.time()
105+
debugToConsole('Ban took longer than expected, sleeping again for', timeToWait, 'seconds.')
106+
self.waitForReset(timeToWait)
107+
108+
def run(self):
109+
self.waitForReset(self.timeToWait)
110+
92111

93112
class receiveThread (threading.Thread):
94113
def __init__(self, ws, wsObject):
@@ -103,8 +122,8 @@ def run(self):
103122
self.wsObject.reconnect = True
104123
self.wsObject.authenticated = False
105124
time.sleep(self.wsObject.reconnectTimer)
106-
self.wsObject.reconnectTimer = self.wsObject.reconnectTimer * 2
107125
debugToConsole("we have just set reconnect to true and have waited for " + str(self.wsObject.reconnectTimer))
126+
self.wsObject.reconnectTimer = self.wsObject.reconnectTimer * 2
108127
except KeyboardInterrupt:
109128
debugToConsole("We caught keyboard interrupt in the websocket thread.")
110129

@@ -114,6 +133,8 @@ def __init__(self, options = {}):
114133
self.ACCESSWINDOW = None
115134
self.APIKEY = ''
116135
self.APISECRET = ''
136+
self.rateLimitRemaining = 1000
137+
self.rateLimitReset = 0
117138
global debugging
118139
debugging = False
119140
for key in options:
@@ -129,9 +150,38 @@ def __init__(self, options = {}):
129150
self.ACCESSWINDOW = 10000
130151
self.base = 'https://api.bitvavo.com/v2'
131152

153+
def getRemainingLimit(self):
154+
return self.rateLimitRemaining
155+
156+
def updateRateLimit(self, response):
157+
if 'errorCode' in response:
158+
if (response['errorCode'] == 105):
159+
self.rateLimitRemaining = 0
160+
self.rateLimitReset = int(response['error'].split(' at ')[1].split('.')[0])
161+
timeToWait = (self.rateLimitReset / 1000) - time.time()
162+
if(not hasattr(self, 'rateLimitThread')):
163+
self.rateLimitThread = rateLimitThread(timeToWait, self)
164+
self.rateLimitThread.daemon = True
165+
self.rateLimitThread.start()
166+
# setTimeout(checkLimit, timeToWait)
167+
if ('bitvavo-ratelimit-remaining' in response):
168+
self.rateLimitRemaining = int(response['bitvavo-ratelimit-remaining'])
169+
if ('bitvavo-ratelimit-resetat' in response):
170+
self.rateLimitReset = int(response['bitvavo-ratelimit-resetat'])
171+
timeToWait = (self.rateLimitReset / 1000) - time.time()
172+
if(not hasattr(self, 'rateLimitThread')):
173+
self.rateLimitThread = rateLimitThread(timeToWait, self)
174+
self.rateLimitThread.daemon = True
175+
self.rateLimitThread.start()
176+
177+
132178
def publicRequest(self, url):
133179
debugToConsole("REQUEST: " + url)
134180
r = requests.get(url)
181+
if('error' in r.json()):
182+
self.updateRateLimit(r.json())
183+
else:
184+
self.updateRateLimit(r.headers)
135185
return r.json()
136186

137187
def privateRequest(self, endpoint, postfix, body = {}, method = 'GET'):
@@ -147,20 +197,17 @@ def privateRequest(self, endpoint, postfix, body = {}, method = 'GET'):
147197
debugToConsole("REQUEST: " + url)
148198
if(method == 'GET'):
149199
r = requests.get(url, headers = headers)
150-
print(r)
151-
return r.json()
152200
elif(method == 'DELETE'):
153201
r = requests.delete(url, headers = headers)
154-
print(r)
155-
return r.json()
156202
elif(method == 'POST'):
157203
r = requests.post(url, headers = headers, json = body)
158-
print(r)
159-
return r.json()
160204
elif(method == 'PUT'):
161205
r = requests.put(url, headers = headers, json = body)
162-
print(r)
163-
return r.json()
206+
if('error' in r.json()):
207+
self.updateRateLimit(r.json())
208+
else:
209+
self.updateRateLimit(r.headers)
210+
return r.json()
164211

165212
def time(self):
166213
return self.publicRequest((self.base + '/time'))
@@ -278,10 +325,10 @@ def withdrawalHistory(self, options):
278325
return self.privateRequest('/withdrawalHistory', postfix, {}, 'GET')
279326

280327
def newWebsocket(self):
281-
return Bitvavo.websocket(self.APIKEY, self.APISECRET, self.ACCESSWINDOW)
328+
return Bitvavo.websocket(self.APIKEY, self.APISECRET, self.ACCESSWINDOW, self)
282329

283330
class websocket:
284-
def __init__(self, APIKEY, APISECRET, ACCESSWINDOW):
331+
def __init__(self, APIKEY, APISECRET, ACCESSWINDOW, bitvavo):
285332
self.APIKEY = APIKEY
286333
self.APISECRET = APISECRET
287334
self.ACCESSWINDOW = ACCESSWINDOW
@@ -290,6 +337,7 @@ def __init__(self, APIKEY, APISECRET, ACCESSWINDOW):
290337
self.keepAlive = True
291338
self.reconnect = False
292339
self.reconnectTimer = 0.1
340+
self.bitvavo = bitvavo
293341

294342
self.subscribe()
295343

@@ -336,6 +384,8 @@ def on_message(ws, msg):
336384
callbacks = ws.callbacks
337385

338386
if('error' in msg):
387+
if (msg['errorCode'] == 105):
388+
ws.bitvavo.updateRateLimit(msg)
339389
if('error' in callbacks):
340390
callbacks['error'](msg)
341391
else:
@@ -455,7 +505,7 @@ def checkReconnect(self):
455505
def on_open(self):
456506
now = int(time.time()*1000)
457507
self.open = True
458-
self.reconnectTimer = 0.1
508+
self.reconnectTimer = 0.5
459509
if(self.APIKEY != ''):
460510
self.doSend(self.ws, json.dumps({ 'window':str(self.ACCESSWINDOW), 'action': 'authenticate', 'key': self.APIKEY, 'signature': createSignature(now, 'GET', '/websocket', {}, self.APISECRET), 'timestamp': now }))
461511
if self.reconnect:

testApi.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from pythonBitvavoApi.bitvavo import Bitvavo
1+
from bitvavo import Bitvavo
22
import sys
33
import signal
44
import time
@@ -24,8 +24,11 @@ def main():
2424
testWebsockets(bitvavo)
2525

2626
def testREST(bitvavo):
27+
limit = bitvavo.getRemainingLimit()
28+
print('Remaining ratelimit is', limit)
29+
2730
response = bitvavo.time()
28-
# print('Current time:', response['time'])
31+
print(response)
2932

3033
# response = bitvavo.markets({})
3134
# for market in response:
@@ -150,9 +153,11 @@ def testWebsockets(bitvavo):
150153
# websocket.subscriptionBookUpdate('BTC-EUR', callback)
151154

152155
# websocket.subscriptionBook('BTC-EUR', callback)
156+
limit = bitvavo.getRemainingLimit()
153157
try:
154-
while(True):
155-
time.sleep(2)
158+
while(limit > 0):
159+
time.sleep(0.5)
160+
limit = bitvavo.getRemainingLimit()
156161
except KeyboardInterrupt:
157162
websocket.closeSocket()
158163

0 commit comments

Comments
 (0)