Last active
May 9, 2024 07:34
-
-
Save normanlmfung/b4a0354d3f8fcba440d9b3716f407be0 to your computer and use it in GitHub Desktop.
python_syntax_ws_pinned_memory
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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