Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ dist
### IDEs ###
.idea
.vscode

/build/
/dist
152 changes: 126 additions & 26 deletions python_bitvavo_api/bitvavo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand All @@ -157,11 +162,17 @@ 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

def getRemainingLimit(self):
if self.rateLimitRemaining < 999 and round(time.time()) % 60 == 0:
self.time()
return self.rateLimitRemaining

def updateRateLimit(self, response):
Expand All @@ -173,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)
Expand Down Expand Up @@ -278,27 +329,38 @@ def ticker24h(self, options=None):
# optional body parameters: limit:(amount, price, postOnly), market:(amount, amountQuote, disableMarketProtection)
# stopLoss/takeProfit:(amount, amountQuote, disableMarketProtection, triggerType, triggerReference, triggerAmount)
# stopLossLimit/takeProfitLimit:(amount, price, postOnly, triggerType, triggerReference, triggerAmount)
# all orderTypes: timeInForce, selfTradePrevention, responseRequired
# all orderTypes: timeInForce, selfTradePrevention, responseRequired, operatorId
def placeOrder(self, market, side, orderType, body):
body['market'] = market
body['side'] = side
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)
# untriggered stopLoss/takeProfit:(amount, amountQuote, disableMarketProtection, triggerType, triggerReference, triggerAmount)
# stopLossLimit/takeProfitLimit: (amount, price, postOnly, triggerType, triggerReference, triggerAmount)
# all orderTypes: operatorId
def updateOrder(self, market, orderId, body):
body['market'] = market
body['orderId'] = orderId
return self.privateRequest('/order', '', body, 'PUT')

def cancelOrder(self, market, orderId):
postfix = createPostfix({ 'market': market, 'orderId': orderId})
def cancelOrder(self, market, orderId, operatorId=None):
params = {'market': market, 'orderId': orderId}
if operatorId is not None:
params['operatorId'] = operatorId
postfix = createPostfix(params)
return self.privateRequest('/order', postfix, {}, 'DELETE')

# options: limit, start, end, orderIdFrom, orderIdTo
Expand Down Expand Up @@ -361,6 +423,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)

Expand Down Expand Up @@ -390,6 +456,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
Expand All @@ -414,6 +481,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):
Expand Down Expand Up @@ -523,9 +591,9 @@ def on_error(self, ws, error):
else:
errorToConsole(error)

def on_close(self, ws):
self.receiveThread.exit()
debugToConsole('Closed Websocket.')
def on_close(self, ws, close_status_code, close_msg):
# self.receiveThread.exit()
debugToConsole(f'Closed Websocket (Status: {close_status_code} Message: {close_msg}).')

def checkReconnect(self):
if('subscriptionTicker' in self.callbacks):
Expand Down Expand Up @@ -624,7 +692,7 @@ def tickerBook(self, options, callback):
# optional body parameters: limit:(amount, price, postOnly), market:(amount, amountQuote, disableMarketProtection)
# stopLoss/takeProfit:(amount, amountQuote, disableMarketProtection, triggerType, triggerReference, triggerAmount)
# stopLossLimit/takeProfitLimit:(amount, price, postOnly, triggerType, triggerReference, triggerAmount)
# all orderTypes: timeInForce, selfTradePrevention, responseRequired
# all orderTypes: timeInForce, selfTradePrevention, responseRequired, operatorId
def placeOrder(self, market, side, orderType, body, callback):
self.callbacks['placeOrder'] = callback
body['market'] = market
Expand All @@ -641,16 +709,19 @@ def getOrder(self, market, orderId, callback):
# Optional parameters: limit:(amount, amountRemaining, price, timeInForce, selfTradePrevention, postOnly)
# untriggered stopLoss/takeProfit:(amount, amountQuote, disableMarketProtection, triggerType, triggerReference, triggerAmount)
# stopLossLimit/takeProfitLimit: (amount, price, postOnly, triggerType, triggerReference, triggerAmount)
# all orderTypes: operatorId
def updateOrder(self, market, orderId, body, callback):
self.callbacks['updateOrder'] = callback
body['market'] = market
body['orderId'] = orderId
body['action'] = 'privateUpdateOrder'
self.doSend(self.ws, json.dumps(body), True)

def cancelOrder(self, market, orderId, callback):
def cancelOrder(self, market, orderId, callback, operatorId=None):
self.callbacks['cancelOrder'] = callback
options = { 'action': 'privateCancelOrder', 'market': market, 'orderId': orderId }
if operatorId is not None:
options['operatorId'] = operatorId
self.doSend(self.ws, json.dumps(options), True)

# options: limit, start, end, orderIdFrom, orderIdTo
Expand Down Expand Up @@ -724,34 +795,63 @@ 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:
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:
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:
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.3",
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(),
Expand Down