Skip to content

Instantly share code, notes, and snippets.

@normanlmfung
Last active May 9, 2024 07:34
Show Gist options
  • Save normanlmfung/b4a0354d3f8fcba440d9b3716f407be0 to your computer and use it in GitHub Desktop.
Save normanlmfung/b4a0354d3f8fcba440d9b3716f407be0 to your computer and use it in GitHub Desktop.
python_syntax_ws_pinned_memory
import asyncio
import json
import struct
from websockets import connect
'''
https://gist.github.com/normanlmfung/b4a0354d3f8fcba440d9b3716f407be0
OKX API
OKX REST API addresses:
REST: https://www.okx.com/
Public WebSocket: wss://ws.okx.com:8443/ws/v5/public
Private WebSocket: wss://ws.okx.com:8443/ws/v5/private
Business WebSocket: wss://ws.okx.com:8443/ws/v5/business
GET Orderbooks endpoint:
GET /api/v5/market/books?instId=BTC-USDT
Below example to subscribe for orderbook updates for 'BTC-USDT-SWAP'.
ws base url: wss://ws.okx.com:8443/ws/v5/public
To subscribe for orderbook:
{
"op": "subscribe",
"args": [
{
"channel": "books",
"instId": "BTC-USDT-SWAP"
}
]
}
It's a good idea to send a 'ping' to OKX every 100 updates received, to keep ws connection alive.
Received message:
{"arg":{"channel":"books","instId":"BTC-USDT-SWAP"},"action":"update","data":[{"asks":[["69291.7","507","0","31"],["69293.1","0","0","0"],["69295.1","1","0","1"],["69295.8","0","0","0"],["69304","11","0","2"],["69304.1","0","0","0"],["69306.8","1","0","1"],["69306.9","17","0","2"],["69307.3","1","0","1"],["69308.6","0","0","0"],["69309.9","40","0","1"],["69310.7","7","0","1"],["69310.8","11","0","2"],["69310.9","2","0","2"],["69311","3","0","1"],["69311.1","49","0","2"],["69311.8","20","0","3"],["69311.9","0","0","0"],["69312","243","0","2"],["69312.1","27","0","4"],["69312.3","0","0","0"],["69312.4","836","0","5"],["69314.5","47","0","2"],["69315.8","81","0","4"] ...
There can be two kinds of responses:
Push Data Example: Full Snapshot
{
"arg": {
"channel": "books",
"instId": "BTC-USDT"
},
"action": "snapshot",
"data": [
{
"asks": [
["8476.98", "415", "0", "13"],
["8477", "7", "0", "2"],
["8477.34", "85", "0", "1"],
["8477.56", "1", "0", "1"],
["8505.84", "8", "0", "1"],
["8506.37", "85", "0", "1"],
["8506.49", "2", "0", "1"],
["8506.96", "100", "0", "2"]
],
"bids": [
["8476.97", "256", "0", "12"],
["8475.55", "101", "0", "1"],
["8475.54", "100", "0", "1"],
["8475.3", "1", "0", "1"],
["8447.32", "6", "0", "1"],
["8447.02", "246", "0", "1"],
["8446.83", "24", "0", "1"],
["8446", "95", "0", "3"]
],
"ts": "1597026383085",
"checksum": -855196043,
"prevSeqId": -1,
"seqId": 123456
}
]
}
Push Data Example: Incremental Data
{
"arg": {
"channel": "books",
"instId": "BTC-USDT"
},
"action": "update",
"data": [
{
"asks": [
["8476.98", "415", "0", "13"],
["8477", "7", "0", "2"],
["8477.34", "85", "0", "1"],
["8477.56", "1", "0", "1"],
["8505.84", "8", "0", "1"],
["8506.37", "85", "0", "1"],
["8506.49", "2", "0", "1"],
["8506.96", "100", "0", "2"]
],
"bids": [
["8476.97", "256", "0", "12"],
["8475.55", "101", "0", "1"],
["8475.54", "100", "0", "1"],
["8475.3", "1", "0", "1"],
["8447.32", "6", "0", "1"],
["8447.02", "246", "0", "1"],
["8446.83", "24", "0", "1"],
["8446", "95", "0", "3"]
],
"ts": "1597026383085",
"checksum": -855196043,
"prevSeqId": 123456,
"seqId": 123457
}
]
}
Further, an example of the array of asks and bids values: ["8476.98", "415", "0", "13"]
- "8476.98" is the depth price
- "415" is the quantity at the price (number of contracts for derivatives, quantity in base currency for Spot and Spot Margin)
- "0" is part of a deprecated feature and it is always "0" (i.e. useless field))
- "13" is the number of orders at the price.
REF:
https://www.okx.com/docs-v5/en/
https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-order-book
https://www.okx.com/docs-v5/en/#overview-websocket-subscribe
https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-order-book-channelOKX API
OKX REST API addresses:
REST: https://www.okx.com/
Public WebSocket: wss://ws.okx.com:8443/ws/v5/public
Private WebSocket: wss://ws.okx.com:8443/ws/v5/private
Business WebSocket: wss://ws.okx.com:8443/ws/v5/business
GET Orderbooks endpoint:
GET /api/v5/market/books?instId=BTC-USDT
Below example to subscribe for orderbook updates for 'BTC-USDT-SWAP'.
ws base url: wss://ws.okx.com:8443/ws/v5/public
To subscribe for orderbook:
{
"op": "subscribe",
"args": [
{
"channel": "books",
"instId": "BTC-USDT-SWAP"
}
]
}
It's a good idea to send a 'ping' to OKX every 100 updates received, to keep ws connection alive.
Received message:
{"arg":{"channel":"books","instId":"BTC-USDT-SWAP"},"action":"update","data":[{"asks":[["69291.7","507","0","31"],["69293.1","0","0","0"],["69295.1","1","0","1"],["69295.8","0","0","0"],["69304","11","0","2"],["69304.1","0","0","0"],["69306.8","1","0","1"],["69306.9","17","0","2"],["69307.3","1","0","1"],["69308.6","0","0","0"],["69309.9","40","0","1"],["69310.7","7","0","1"],["69310.8","11","0","2"],["69310.9","2","0","2"],["69311","3","0","1"],["69311.1","49","0","2"],["69311.8","20","0","3"],["69311.9","0","0","0"],["69312","243","0","2"],["69312.1","27","0","4"],["69312.3","0","0","0"],["69312.4","836","0","5"],["69314.5","47","0","2"],["69315.8","81","0","4"] ...
There can be two kinds of responses:
Push Data Example: Full Snapshot
{
"arg": {
"channel": "books",
"instId": "BTC-USDT"
},
"action": "snapshot",
"data": [
{
"asks": [
["8476.98", "415", "0", "13"],
["8477", "7", "0", "2"],
["8477.34", "85", "0", "1"],
["8477.56", "1", "0", "1"],
["8505.84", "8", "0", "1"],
["8506.37", "85", "0", "1"],
["8506.49", "2", "0", "1"],
["8506.96", "100", "0", "2"]
],
"bids": [
["8476.97", "256", "0", "12"],
["8475.55", "101", "0", "1"],
["8475.54", "100", "0", "1"],
["8475.3", "1", "0", "1"],
["8447.32", "6", "0", "1"],
["8447.02", "246", "0", "1"],
["8446.83", "24", "0", "1"],
["8446", "95", "0", "3"]
],
"ts": "1597026383085",
"checksum": -855196043,
"prevSeqId": -1,
"seqId": 123456
}
]
}
Push Data Example: Incremental Data
{
"arg": {
"channel": "books",
"instId": "BTC-USDT"
},
"action": "update",
"data": [
{
"asks": [
["8476.98", "415", "0", "13"],
["8477", "7", "0", "2"],
["8477.34", "85", "0", "1"],
["8477.56", "1", "0", "1"],
["8505.84", "8", "0", "1"],
["8506.37", "85", "0", "1"],
["8506.49", "2", "0", "1"],
["8506.96", "100", "0", "2"]
],
"bids": [
["8476.97", "256", "0", "12"],
["8475.55", "101", "0", "1"],
["8475.54", "100", "0", "1"],
["8475.3", "1", "0", "1"],
["8447.32", "6", "0", "1"],
["8447.02", "246", "0", "1"],
["8446.83", "24", "0", "1"],
["8446", "95", "0", "3"]
],
"ts": "1597026383085",
"checksum": -855196043,
"prevSeqId": 123456,
"seqId": 123457
}
]
}
Further, an example of the array of asks and bids values: ["8476.98", "415", "0", "13"]
- "8476.98" is the depth price
- "415" is the quantity at the price (number of contracts for derivatives, quantity in base currency for Spot and Spot Margin)
- "0" is part of a deprecated feature and it is always "0" (i.e. useless field))
- "13" is the number of orders at the price.
REF:
https://www.okx.com/docs-v5/en/
https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-order-book
https://www.okx.com/docs-v5/en/#overview-websocket-subscribe
https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-order-book-channel
'''
class Level:
def __init__(self, price, amount, num_orders):
self.Price = price
self.Amount = amount
self.NumOrders = num_orders
class OrderBookUpdate:
def __init__(self, symbol):
self.Symbol = symbol
self.Bids = []
self.Asks = []
self.Timestamp = 0
self.PrevSeqId = 0
self.SeqId = 0
self.Checksum = 0
@staticmethod
def parse_update(symbol, message_object):
update = OrderBookUpdate(symbol)
update.Asks = OrderBookUpdate.parse_levels(message_object['data'][0]['asks'])
update.Bids = OrderBookUpdate.parse_levels(message_object['data'][0]['bids'])
update.Timestamp = message_object['data'][0]['ts']
update.PrevSeqId = message_object['data'][0]['prevSeqId']
update.SeqId = message_object['data'][0]['seqId']
update.Checksum = message_object['data'][0]['checksum']
return update
@staticmethod
def parse_levels(levels):
parsed_levels = []
for level in levels:
parsed_levels.append(Level(float(level[0]), float(level[1]), int(level[3])))
return parsed_levels
class OrderBook:
def __init__(self, symbol, last_timestamp):
self.Symbol = symbol.strip()
self.LastTimeStamp = last_timestamp
self.Bids = []
self.Asks = []
@staticmethod
def parse_snapshot(symbol, message_object):
order_book = OrderBook(symbol, int(message_object['data'][0]['ts']))
for ask in message_object['data'][0]['asks']:
order_book.Asks.append(Level(float(ask[0]), float(ask[1]), int(ask[3])))
for bid in message_object['data'][0]['bids']:
order_book.Bids.append(Level(float(bid[0]), float(bid[1]), int(bid[3])))
return order_book
def get_mid_price(self):
if self.Bids and self.Asks:
best_bid = self.Bids[0].Price
best_ask = self.Asks[0].Price
return (best_bid + best_ask) / 2
return 0
def update_book(self, update):
self.update_levels(self.Bids, update.Bids)
self.update_levels(self.Asks, update.Asks)
def update_levels(self, current_levels, new_levels):
for new_level in new_levels:
existing_level = next((l for l in current_levels if l.Price == new_level.Price), None)
if existing_level:
if new_level.Amount == 0:
current_levels.remove(existing_level)
else:
existing_level.Amount = new_level.Amount
existing_level.NumOrders = new_level.NumOrders
else:
current_levels.append(new_level)
current_levels.sort(key=lambda x: x.Price)
async def connect_and_subscribe():
uri = "wss://ws.okx.com:8443/ws/v5/public"
symbol = "BTC-USDT-SWAP"
subscribe_books_request = json.dumps({"op": "subscribe", "args": [{"channel": "books", "instId": symbol}]})
subscribe_books5_request = json.dumps({"op": "subscribe", "args": [{"channel": "books5", "instId": symbol}]})
async with connect(uri) as websocket:
await websocket.send(subscribe_books_request)
await websocket.recv()
await websocket.send(subscribe_books5_request)
order_book = None
order_book_buffer = allocate_memory(symbol) # type(order_book_buffer): class 'bytearray'
while True:
received_message = await websocket.recv()
message_object = json.loads(received_message)
if 'action' in message_object and message_object['action'] == 'snapshot':
original_order_book = OrderBook.parse_snapshot(symbol, message_object)
serialize_order_book(symbol, original_order_book, order_book_buffer)
order_book = deserialize_order_book(symbol, order_book_buffer, len(order_book_buffer),
len(original_order_book.Bids), len(original_order_book.Asks))
elif 'action' in message_object and message_object['action'] == 'update':
if order_book is not None:
update = OrderBookUpdate.parse_update(symbol, message_object)
order_book.LastTimeStamp = update.Timestamp
order_book.update_book(update)
if order_book is not None:
print(f"{symbol} mid: {order_book.get_mid_price()}")
def allocate_memory(symbol):
MAX_BID_COUNT = 1000
MAX_ASK_COUNT = 1000
size = len(symbol) * 2 + 8 + 12 * MAX_BID_COUNT + 12 * MAX_ASK_COUNT
return bytearray(size)
def serialize_order_book(symbol, order_book, buffer):
offset = 0
symbol_bytes = symbol.encode()
struct.pack_into(f"{len(symbol_bytes)}s", buffer, offset, symbol_bytes)
offset += len(symbol_bytes)
struct.pack_into("q", buffer, offset, order_book.LastTimeStamp)
offset += 8
num_bids = len(order_book.Bids)
struct.pack_into("i", buffer, offset, num_bids)
offset += 4
for bid in order_book.Bids:
struct.pack_into("ffi", buffer, offset, bid.Price, bid.Amount, bid.NumOrders)
offset += 12
num_asks = len(order_book.Asks)
struct.pack_into("i", buffer, offset, num_asks)
offset += 4
for ask in order_book.Asks:
struct.pack_into("ffi", buffer, offset, ask.Price, ask.Amount, ask.NumOrders)
offset += 12
def deserialize_order_book(symbol, buffer, length, num_bids, num_asks):
offset = len(symbol)
timestamp = struct.unpack_from("q", buffer, offset)[0]
order_book = OrderBook(symbol, timestamp)
offset += 8
for _ in range(num_bids):
price, amount, num_orders = struct.unpack_from("ffi", buffer, offset)
order_book.Bids.append(Level(price, amount, num_orders))
offset += 12
for _ in range(num_asks):
price, amount, num_orders = struct.unpack_from("ffi", buffer, offset)
order_book.Asks.append(Level(price, amount, num_orders))
offset += 12
return order_book
asyncio.get_event_loop().run_until_complete(connect_and_subscribe())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment