Websocket Overview
The WebSocket feed needs to be authenticated. It provides real-time market data and updates for orders and trades.
INFOProduction endpoint:
wss://dma-ws.zerocap.com/v2Sandbox endpoint:
wss://sandbox-ws.zerocap.com/v2
Protocol
The WebSocket feed uses a bidirectional protocol that encodes all messages as JSON objects. All messages have a type attribute that can be used to handle the message appropriately.
Sending Messages
Authentication
To authenticate, the initial connection must contain the same headers as a REST request. After the connection is authenticated there is no further need to send the headers.
import hmac
import json
import time
import hashlib
import requests
import threading
from websockets.sync.client import connect
class ZerocapWebsocketClient:
def __init__(self, api_key: str, api_secret: str, env: str='prod'):
self.api_key = api_key
self.api_secret = api_secret
self.websocket = None
if env == 'prod':
self.base_url = "wss://dma-ws.zerocap.com/v2"
self.http_url = "https://dma-api.zerocap.com/v2/orders"
elif env == 'sandbox':
self.base_url = "wss://sandbox-ws.zerocap.com/v2"
self.http_url = "https://sandbox-api.zerocap.com/v2/orders"
def verify_identity(self):
timestamp = int(time.time())
headers = {'Content-Type': 'application/json'}
data = {"api_key": self.api_key, "signature": self.hashing(timestamp)}
url = f"{self.http_url}/api_key_signature_valid"
response = requests.post(url, data=json.dumps(data), headers=headers)
if response.status_code != 200 or response.json().get('status_code') != 200:
raise Exception("Authentication failed")
def hashing(self, timestamp):
return hmac.new(
self.api_secret.encode("utf-8"), str(timestamp).encode("utf-8"), hashlib.sha256
).hexdigest()
def close(self):
try:
if self.websocket:
self.websocket.close()
except:
pass
def recv(self, websocket):
return json.loads(websocket.__next__())
def send(self, message: json):
try:
self.websocket.send(json.dumps(message))
except Exception as e:
raise Exception(e)
def test_send_heartbeat(self):
while True:
time.sleep(5)
if not self.websocket:
continue
try:
self.websocket.send(json.dumps({"type": "message", "message": "ping"}))
except Exception as e:
raise Exception(e)
return
def create_connection(self, timestamp: int = 0):
if not timestamp:
timestamp = int(time.time())
try:
threading.Thread(target=self.test_send_heartbeat).start()
with connect(self.base_url,
additional_headers={"api-key": self.api_key, "signature": self.hashing(timestamp), "timestamp": str(timestamp)}
) as self.websocket:
while True:
yield self.websocket.recv()
except Exception as e:
self.close()
raise Exception(e)Subscribing
To begin receiving feed messages, you must send a subscribe message to the server indicating which channel and products to receive. You can subscribe to multiple channels but you must send a unique subscription message for each channel.
Channels
Heartbeat
You must send a heartbeat every 30 seconds or you will be disconnected.
// Request
{
"type": "message",
"message": "ping"
}A heartbeat message is of the type message and has one parameter:
message- will always have valuepong.
// Response
{
"type": "message",
"message": "pong"
}Market Data
The price channel streams executable pricing for a given market.
// Request
{
"type": "price",
"symbols": ["USDT/AUD"]
}Multiple markets can also be subscribed at the same time.
// Request
{
"type": "price",
"symbols": ["USDT/AUD", "USDT/USD"]
}The price channel sends a message with the following fields:
timestampis the time of the event as recorded by our trading engine.symbolis the market for which pricing is being streamed.bidsandasksare pricing for selling and buying. Each level is arranged with the best price as the first item. Within each item the first value is the price, and the second value is the quantity for which the price is valid.
INFOThe returned pricing is not an order book. Placing an order greater than one or more of the levels will fill at the appropriate level, instead of sweeping each level in order. For example, for the below response, placing a sell order for 300,000 USDT at price 1.5136 will fill the entire order at 1.5136, instead of filling 250,000 at 1.5137 and the remainder at 1.5136.
// Response
{
"type":"price",
"data":{
"timestamp":1706725399.280178,
"symbol":"USDT/AUD",
"exchange":"zerocap",
"bids":[
[
1.5137,
250000
],
[
1.5136,
500000
]
],
"asks":[
[
1.5192,
250000
],
[
1.5193,
500000
]
]
}
}Orders
The orders channel streams order updates.
// Request
{
"type": "order"
}The order channel sends a message with the following fields:
timestampis the time of the event as recorded by our trading engine.last_trade_timestampis the Unix timestamp of the most recent trade on this order.idis the order ID.symbolis the market for which the order was placed.statusis the current status of the order. The available statuses are 'open', 'closed', 'canceled', and 'rejected'.typeis the type of order.time_in_forceindicates how long an order will remain active before it is executed or expires.sideis the direction of the order.priceis the price of the order if it was a limit order. It is distinct from the price the order was filled at.averageis the average price the order was filled at.amountis the size of the base asset the order was placed for.filledis the amount of base asset that was executed.remainingis the amount of base asset that has not been executed.costis the size of the quote asset the order the order was filled at. It will be zero if the order was not filled.tradesare the fills that correspond to a given order.feeis the fee charged for an order.client_order_idis an optional field for the user to give placed orders a unique identifier.
// Response
{
"type":"order",
"data":{
"id":"f3a1fdab-6aa0-4ea9-ad37-0d9ac62d3090",
"timestamp":1706721454758,
"last_trade_timestamp":1706721454758,
"status":"open",
"symbol":"USDT/AUD",
"type":"limit",
"time_in_force":"FOK",
"side":"sell",
"price":1.4977,
"average":1.5129,
"amount":100.0,
"filled":100.0,
"remaining":0.0,
"cost":149.77,
"error_message":"",
"fee":"0",
"trades":[],
"client_order_id":"79de03ab-c2ec-4dd5-8bce-29cbe1b70fca"
}
}Trade
The trades channel streams fills
// Request
{
"type": "trade"
}The trades channel sends a message with the following fields:
timestampis the time of the event as recorded by our trading engine.idis the trade ID.orderis the order ID associated with the fill.symbolis the market for which the order was placed.typeis the type of order.sideis the direction of the order.priceis the average price the order was filled at.amountis the size of the base asset the order was placed for.costis the size of the quote asset the order the trade was filled at.taker_or_makerindicates whether the order was filled as an aggressor.feeis the fee charged for an order.client_order_idis an optional field for the user to give placed orders a unique identifier.
// Response
{
"type":"trade",
"data":{
"id":"73a8e6c0-7205-4953-b2a5-aabac01216f3",
"timestamp":1706721454758,
"symbol":"USDT/AUD",
"order":"f3a1fdab-6aa0-4ea9-ad37-0d9ac62d3090",
"type":"limit",
"side":"sell",
"taker_or_maker":"taker",
"price":1.5129,
"amount":100.0,
"cost":151.29,
"fee":"0",
"client_order_id":"79de03ab-c2ec-4dd5-8bce-29cbe1b70fca"
}
}Updated 10 months ago