From 700b05b6a0b561dbc24c019bff4d4a883f9cf88b Mon Sep 17 00:00:00 2001 From: cibcbv <159249965+cibcbv@users.noreply.github.com> Date: Mon, 2 Sep 2024 21:23:58 +0200 Subject: [PATCH 01/10] Make subscriptionTicker handle multiple markets Make subscriptionTicker handle multiple markets --- python_bitvavo_api/bitvavo.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/python_bitvavo_api/bitvavo.py b/python_bitvavo_api/bitvavo.py index ed4be09..78d3ad4 100644 --- a/python_bitvavo_api/bitvavo.py +++ b/python_bitvavo_api/bitvavo.py @@ -724,8 +724,14 @@ def withdrawalHistory(self, options, callback): def subscriptionTicker(self, market, callback): if 'subscriptionTicker' not in self.callbacks: self.callbacks['subscriptionTicker'] = {} - self.callbacks['subscriptionTicker'][market] = callback - self.doSend(self.ws, json.dumps({ 'action': 'subscribe', 'channels': [{ 'name': 'ticker', 'markets': [market] }] })) + if type(market) is list: + for i_market in market: + self.callbacks['subscriptionTicker'][i_market] = callback + markets = market + else: + self.callbacks['subscriptionTicker'][market] = callback + markets = [market] + self.doSend(self.ws, json.dumps({ 'action': 'subscribe', 'channels': [{ 'name': 'ticker', 'markets': markets }] })) def subscriptionTicker24h(self, market, callback): if 'subscriptionTicker24h' not in self.callbacks: From 7ca361948f4c8c5faf38f7e5f30dc6fba09a275a Mon Sep 17 00:00:00 2001 From: cibcbv <159249965+cibcbv@users.noreply.github.com> Date: Mon, 2 Sep 2024 21:29:20 +0200 Subject: [PATCH 02/10] Make subscriptionTicker24h handle multiple markets Make subscriptionTicker24h handle multiple markets --- python_bitvavo_api/bitvavo.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/python_bitvavo_api/bitvavo.py b/python_bitvavo_api/bitvavo.py index 78d3ad4..4caafe9 100644 --- a/python_bitvavo_api/bitvavo.py +++ b/python_bitvavo_api/bitvavo.py @@ -727,7 +727,7 @@ def subscriptionTicker(self, market, callback): if type(market) is list: for i_market in market: self.callbacks['subscriptionTicker'][i_market] = callback - markets = market + markets = market else: self.callbacks['subscriptionTicker'][market] = callback markets = [market] @@ -736,8 +736,14 @@ def subscriptionTicker(self, market, callback): def subscriptionTicker24h(self, market, callback): if 'subscriptionTicker24h' not in self.callbacks: self.callbacks['subscriptionTicker24h'] = {} - self.callbacks['subscriptionTicker24h'][market] = callback - self.doSend(self.ws, json.dumps({ 'action': 'subscribe', 'channels': [{ 'name': 'ticker24h', 'markets': [market] }] })) + if type(market) is list: + for i_market in market: + self.callbacks['subscriptionTicker24h'][i_market] = callback + markets = market + else: + self.callbacks['subscriptionTicker24h'][market] = callback + markets = [market] + self.doSend(self.ws, json.dumps({ 'action': 'subscribe', 'channels': [{ 'name': 'ticker24h', 'markets': markets }] })) def subscriptionAccount(self, market, callback): if 'subscriptionAccount' not in self.callbacks: From 2682c6dd66738d15eb4fb780d6c081a04208eb3a Mon Sep 17 00:00:00 2001 From: Jan Copier Date: Wed, 1 Jan 2025 19:33:30 +0100 Subject: [PATCH 03/10] Add enabling ping/pong with parameters ping_interval/ping_timeout --- python_bitvavo_api/bitvavo.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/python_bitvavo_api/bitvavo.py b/python_bitvavo_api/bitvavo.py index 4caafe9..675bc63 100644 --- a/python_bitvavo_api/bitvavo.py +++ b/python_bitvavo_api/bitvavo.py @@ -121,7 +121,10 @@ def __init__(self, ws, wsObject): def run(self): try: while(self.wsObject.keepAlive): - self.ws.run_forever() + self.ws.run_forever( + ping_interval = self.wsObject.bitvavo.ping_interval, + ping_timeout = self.wsObject.bitvavo.ping_timeout + ) self.wsObject.reconnect = True self.wsObject.authenticated = False time.sleep(self.wsObject.reconnectTimer) @@ -141,6 +144,8 @@ def __init__(self, options = {}): self.rateLimitRemaining = 1000 self.rateLimitReset = 0 self.timeout = None + self.ping_interval = None + self.ping_timeout = None global debugging debugging = False for key in options: @@ -157,7 +162,11 @@ def __init__(self, options = {}): elif key.lower() == "wsurl": self.wsUrl = options[key] elif key.lower() == "timeout": - self.timeout = options[key] + self.timeout = options[key] + elif key.lower() == "ping_interval": + self.ping_interval = options[key] + elif key.lower() == "ping_timeout": + self.ping_timeout = options[key] if(self.ACCESSWINDOW == None): self.ACCESSWINDOW = 10000 From ad992b854b9a2c15f14f32b61facb844fd5ff011 Mon Sep 17 00:00:00 2001 From: Jan Copier Date: Wed, 1 Jan 2025 19:43:04 +0100 Subject: [PATCH 04/10] Added ratelimit updating when using websocket. --- python_bitvavo_api/bitvavo.py | 47 +++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/python_bitvavo_api/bitvavo.py b/python_bitvavo_api/bitvavo.py index 675bc63..9f1b0a7 100644 --- a/python_bitvavo_api/bitvavo.py +++ b/python_bitvavo_api/bitvavo.py @@ -171,6 +171,8 @@ def __init__(self, options = {}): self.ACCESSWINDOW = 10000 def getRemainingLimit(self): + if self.rateLimitRemaining < 999 and round(time.time()) % 60 == 0: + self.time() return self.rateLimitRemaining def updateRateLimit(self, response): @@ -182,18 +184,58 @@ def updateRateLimit(self, response): if(not hasattr(self, 'rateLimitThread')): self.rateLimitThread = rateLimitThread(timeToWait, self) self.rateLimitThread.daemon = True + self.rateLimitThread.name = 'Bitvavo.rateLimitThread' self.rateLimitThread.start() # setTimeout(checkLimit, timeToWait) - if ('bitvavo-ratelimit-remaining' in response): + elif ('bitvavo-ratelimit-remaining' in response): self.rateLimitRemaining = int(response['bitvavo-ratelimit-remaining']) - if ('bitvavo-ratelimit-resetat' in response): + elif ('bitvavo-ratelimit-resetat' in response): self.rateLimitReset = int(response['bitvavo-ratelimit-resetat']) timeToWait = (self.rateLimitReset / 1000) - time.time() if(not hasattr(self, 'rateLimitThread')): self.rateLimitThread = rateLimitThread(timeToWait, self) self.rateLimitThread.daemon = True + self.rateLimitThread.name = 'Bitvavo.rateLimitThread' self.rateLimitThread.start() + def updateRateLimitFromWebsocket(self, request): + endpointWeightPoints = { + 'authenticate': 0, # Free + 'getAssets': 1, + 'getBook': 1, + 'getCandles': 1, + 'getMarkets': 1, + 'getTicker24h': [1, 25], # 1 with 'market', 25 without 'market' specified + 'getTickerBook': 1, + 'getTickerPrice': 1, + 'getTime': 1, + 'getTrades': 5, + 'privateCancelOrder': 0, # Free, can still be used even if limit reached + 'privateCancelOrders': 0, # Free, can still be used even if limit reached + 'privateCreateOrder': 1, + 'privateDepositAssets': 1, + 'privateGetAccount': 1, + 'privateGetBalance': 5, + 'privateGetDepositHistory': 5, + 'privateGetFees': 1, + 'privateGetOrder': 1, + 'privateGetOrders': 5, + 'privateGetOrdersOpen': [1, 25], # 1 with 'market', 25 without 'market' specified + 'privateGetTrades': 5, + 'privateGetTransactionHistory': 1, + 'privateGetWithdrawalHistory': 5, + 'privateUpdateOrder': 1, + 'privateWithdrawAssets': 1, + 'subscribe': 1 + } + action = request['action'] + weightPoints = endpointWeightPoints[action] + if type(weightPoints) is list and hasattr(request, 'market'): + weightPoints = weightPoints[1] + elif type(weightPoints) is list: + weightPoints = weightPoints[0] + + self.rateLimitRemaining = self.rateLimitRemaining - weightPoints def publicRequest(self, url): debugToConsole("REQUEST: " + url) @@ -423,6 +465,7 @@ def doSend(self, ws, message, private = False): return self.waitForSocket(ws, message, private) ws.send(message) + self.bitvavo.updateRateLimitFromWebsocket(json.loads(message)) debugToConsole('SENT: ' + message) def on_message(self, ws, msg): From a75b2690a03c871046c6fe3dce2e9b737ddefcf2 Mon Sep 17 00:00:00 2001 From: Jan Copier Date: Wed, 1 Jan 2025 19:46:49 +0100 Subject: [PATCH 05/10] Extended getOrder to support query on clientOrderId. --- python_bitvavo_api/bitvavo.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/python_bitvavo_api/bitvavo.py b/python_bitvavo_api/bitvavo.py index 9f1b0a7..59aaf24 100644 --- a/python_bitvavo_api/bitvavo.py +++ b/python_bitvavo_api/bitvavo.py @@ -336,8 +336,15 @@ def placeOrder(self, market, side, orderType, body): body['orderType'] = orderType return self.privateRequest('/order', '', body, 'POST') - def getOrder(self, market, orderId): - postfix = createPostfix({ 'market': market, 'orderId': orderId }) + def getOrder(self, market, orderId: str = '', clientOrderId: str = ''): + body = { + 'market': market + } + if clientOrderId != '': + body['clientOrderId'] = clientOrderId + else: + body['orderId'] = orderId + postfix = createPostfix(body) return self.privateRequest('/order', postfix, {}, 'GET') # Optional parameters: limit:(amount, amountRemaining, price, timeInForce, selfTradePrevention, postOnly) From 8769dfd03918d706f794a5d06ae019c9b3ebe1e3 Mon Sep 17 00:00:00 2001 From: Jan Copier Date: Wed, 1 Jan 2025 19:50:25 +0100 Subject: [PATCH 06/10] Added method for requesting Transaction History (accountTransactionHistory) --- python_bitvavo_api/bitvavo.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python_bitvavo_api/bitvavo.py b/python_bitvavo_api/bitvavo.py index 59aaf24..30c68b5 100644 --- a/python_bitvavo_api/bitvavo.py +++ b/python_bitvavo_api/bitvavo.py @@ -419,6 +419,10 @@ def withdrawalHistory(self, options=None): postfix = createPostfix(options) return self.privateRequest('/withdrawalHistory', postfix, {}, 'GET') + def accountTransactionHistory(self, options=None): + postfix = createPostfix(options) + return self.privateRequest('/account/history', postfix, {}, 'GET') + def newWebsocket(self): return Bitvavo.websocket(self.APIKEY, self.APISECRET, self.ACCESSWINDOW, self.wsUrl, self) From 45bf3639a4fbff1c45407c839ef851957f1cb083 Mon Sep 17 00:00:00 2001 From: Jan Copier Date: Wed, 1 Jan 2025 19:52:39 +0100 Subject: [PATCH 07/10] Improved management of ReceiveThread for websocket. --- python_bitvavo_api/bitvavo.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python_bitvavo_api/bitvavo.py b/python_bitvavo_api/bitvavo.py index 30c68b5..678abc4 100644 --- a/python_bitvavo_api/bitvavo.py +++ b/python_bitvavo_api/bitvavo.py @@ -452,6 +452,7 @@ def subscribe(self): self.receiveThread = receiveThread(ws, self) self.receiveThread.daemon = True + self.receiveThread.name = 'Bitvavo.websocket.receiveThread' self.receiveThread.start() self.authenticated = False @@ -586,9 +587,9 @@ def on_error(self, ws, error): else: errorToConsole(error) - def on_close(self, ws): + def on_close(self, ws, close_status_code, close_msg): self.receiveThread.exit() - debugToConsole('Closed Websocket.') + debugToConsole(f'Closed Websocket (Status: {close_status_code} Message: {close_msg}).') def checkReconnect(self): if('subscriptionTicker' in self.callbacks): From 78f3d3a75481355d53a9ad03910c441ee56f9fb9 Mon Sep 17 00:00:00 2001 From: Jan Copier Date: Wed, 1 Jan 2025 19:54:27 +0100 Subject: [PATCH 08/10] Added support for lists of markets to multiple API-requests. --- python_bitvavo_api/bitvavo.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/python_bitvavo_api/bitvavo.py b/python_bitvavo_api/bitvavo.py index 678abc4..dd34f60 100644 --- a/python_bitvavo_api/bitvavo.py +++ b/python_bitvavo_api/bitvavo.py @@ -812,22 +812,39 @@ def subscriptionTicker24h(self, market, callback): def subscriptionAccount(self, market, callback): if 'subscriptionAccount' not in self.callbacks: self.callbacks['subscriptionAccount'] = {} - self.callbacks['subscriptionAccount'][market] = callback - self.doSend(self.ws, json.dumps({ 'action': 'subscribe', 'channels': [{ 'name': 'account', 'markets': [market] }] }), True) - def subscriptionCandles(self, market, interval, callback): + if type(market) is list: + for i_market in market: + self.callbacks['subscriptionAccount'][i_market] = callback + markets = market + else: + self.callbacks['subscriptionAccount'][market] = callback + markets = [market] + self.doSend(self.ws, json.dumps({ 'action': 'subscribe', 'channels': [{ 'name': 'account', 'markets': markets }] }), True) + + def subscriptionCandles(self, markets, interval, callback): if 'subscriptionCandles' not in self.callbacks: self.callbacks['subscriptionCandles'] = {} - if market not in self.callbacks['subscriptionCandles']: - self.callbacks['subscriptionCandles'][market] = {} - self.callbacks['subscriptionCandles'][market][interval] = callback + + if not isinstance(markets, list): + markets = [markets] + for market in markets: + if market not in self.callbacks['subscriptionCandles']: + self.callbacks['subscriptionCandles'][market] = {} + self.callbacks['subscriptionCandles'][market][interval] = callback self.doSend(self.ws, json.dumps({ 'action': 'subscribe', 'channels': [{ 'name': 'candles', 'interval': [interval], 'markets': [market] }] })) def subscriptionTrades(self, market, callback): if 'subscriptionTrades' not in self.callbacks: self.callbacks['subscriptionTrades'] = {} - self.callbacks['subscriptionTrades'][market] = callback - self.doSend(self.ws, json.dumps({ 'action': 'subscribe', 'channels': [{ 'name': 'trades', 'markets': [market] }] })) + if type(market) is list: + for i_market in market: + self.callbacks['subscriptionTrades'][i_market] = callback + markets = market + else: + self.callbacks['subscriptionTrades'][market] = callback + markets = [market] + self.doSend(self.ws, json.dumps({ 'action': 'subscribe', 'channels': [{ 'name': 'trades', 'markets': markets }] })) def subscriptionBookUpdate(self, market, callback): if 'subscriptionBookUpdate' not in self.callbacks: From a6ae142d69e8bd831bc43d312ea57233b028fbb4 Mon Sep 17 00:00:00 2001 From: Jan Copier Date: Wed, 1 Jan 2025 20:16:27 +0100 Subject: [PATCH 09/10] Administration to build package. --- .gitignore | 3 +++ setup.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index a5d3ac2..38c4853 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ dist ### IDEs ### .idea .vscode + +/build/ +/dist \ No newline at end of file diff --git a/setup.py b/setup.py index e56b4f7..8c5ac2d 100755 --- a/setup.py +++ b/setup.py @@ -10,8 +10,8 @@ name="python_bitvavo_api", long_description=long_description, long_description_content_type='text/markdown', - version="v1.4.2", - author="Bitvavo", + version="v1.4.2a", + author="Bitvavo/CIBC", description="Use Bitvavo SDK for Python to buy, sell, and store over 200 digital assets on Bitvavo from inside your app.", url="https://github.com/bitvavo/python-bitvavo-api", packages=find_packages(), From b65248c6ece148e0775c588312737b48eac61ce7 Mon Sep 17 00:00:00 2001 From: Jan Copier Date: Wed, 1 Jan 2025 21:13:55 +0100 Subject: [PATCH 10/10] Bugfix 'receiveThread' has no attribute 'exit'. --- python_bitvavo_api/bitvavo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_bitvavo_api/bitvavo.py b/python_bitvavo_api/bitvavo.py index dd34f60..3f129c6 100644 --- a/python_bitvavo_api/bitvavo.py +++ b/python_bitvavo_api/bitvavo.py @@ -588,7 +588,7 @@ def on_error(self, ws, error): errorToConsole(error) def on_close(self, ws, close_status_code, close_msg): - self.receiveThread.exit() + # self.receiveThread.exit() debugToConsole(f'Closed Websocket (Status: {close_status_code} Message: {close_msg}).') def checkReconnect(self):