Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save normanlmfung/5bbd2420251b29150db90aca3e139d3c to your computer and use it in GitHub Desktop.
Save normanlmfung/5bbd2420251b29150db90aca3e139d3c to your computer and use it in GitHub Desktop.
csharp_ws_arraypool
using Newtonsoft.Json;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
/*
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.
To reference Newtonsoft, https://www.nuget.org/packages/Newtonsoft.Json
dotnet add package Newtonsoft.Json
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
{
public float Price { get; set; }
public float Amount { get; set; }
public int NumOrders { get; set; }
}
class OrderBookUpdate
{
public string Symbol { get; }
public List<Level> Bids { get; set; }
public List<Level> Asks { get; set; }
public long Timestamp { get; set; }
public long PrevSeqId { get; set; }
public long SeqId { get; set; }
public long Checksum { get; set; }
public OrderBookUpdate(string symbol)
{
Symbol = symbol;
}
public static OrderBookUpdate ParseUpdate(string symbol, dynamic messageObject)
{
OrderBookUpdate update = new OrderBookUpdate(symbol);
update.Asks = ParseLevels(messageObject.data[0].asks);
update.Bids = ParseLevels(messageObject.data[0].bids);
update.Timestamp = messageObject.data[0].ts;
update.PrevSeqId = messageObject.data[0].prevSeqId;
update.SeqId = messageObject.data[0].seqId;
update.Checksum = messageObject.data[0].checksum;
return update;
}
private static List<Level> ParseLevels(dynamic levels)
{
List<Level> parsedLevels = new List<Level>();
foreach (var level in levels)
{
parsedLevels.Add(new Level
{
Price = float.Parse(level[0].ToString()),
Amount = float.Parse(level[1].ToString()),
NumOrders = int.Parse(level[3].ToString())
});
}
return parsedLevels;
}
}
class OrderBook
{
public string Symbol { get; }
public long LastTimeStamp { get; set; } // For example, 1711941149203, 13 digits (i.e. in milli-sec)
public List<Level> Bids { get; set; }
public List<Level> Asks { get; set; }
public OrderBook(string symbol, long lastTimestamp)
{
Symbol = symbol.Trim();
LastTimeStamp = lastTimestamp;
Bids = new List<Level>();
Asks = new List<Level>();
}
public static OrderBook ParseSnapshot(string symbol, dynamic messageObject)
{
OrderBook orderBook = new OrderBook(symbol, Convert.ToInt64(messageObject.data[0].ts));
foreach (var ask in messageObject.data[0].asks)
{
orderBook.Asks.Add(new Level
{
Price = float.Parse(ask[0].ToString()),
Amount = float.Parse(ask[1].ToString()),
NumOrders = int.Parse(ask[3].ToString())
});
}
foreach (var bid in messageObject.data[0].bids)
{
orderBook.Bids.Add(new Level
{
Price = float.Parse(bid[0].ToString()),
Amount = float.Parse(bid[1].ToString()),
NumOrders = int.Parse(bid[3].ToString())
});
}
return orderBook;
}
public float GetMidPrice()
{
if (Bids.Count > 0 && Asks.Count > 0)
{
float bestBid = Bids[0].Price;
float bestAsk = Asks[0].Price;
return (bestBid + bestAsk) / 2;
}
return 0;
}
public void UpdateBook(OrderBookUpdate update)
{
UpdateLevels(Bids, update.Bids);
UpdateLevels(Asks, update.Asks);
}
private void UpdateLevels(List<Level> currentLevels, List<Level> newLevels)
{
foreach (var newLevel in newLevels)
{
var existingLevel = currentLevels.Find(l => l.Price == newLevel.Price);
if (existingLevel != null)
{
if (newLevel.Amount == 0)
{
currentLevels.Remove(existingLevel);
}
else
{
existingLevel.Amount = newLevel.Amount;
existingLevel.NumOrders = newLevel.NumOrders;
}
}
else
{
currentLevels.Add(newLevel);
}
}
currentLevels.Sort((a, b) => a.Price.CompareTo(b.Price));
}
}
class Program
{
static void Log(string message)
{
string utcDateTime = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss");
Console.WriteLine($"{utcDateTime} {message}");
}
static async Task Main(string[] args)
{
string baseUrl = "wss://ws.okx.com:8443/ws/v5/public";
string symbol = "BTC-USDT-SWAP";
string subscribeBooksRequest = "{\"op\":\"subscribe\",\"args\":[{\"channel\":\"books\",\"instId\":\"" + symbol + "\"}]}";
string subscribeBooks5Request = "{\"op\":\"subscribe\",\"args\":[{\"channel\":\"books5\",\"instId\":\"" + symbol + "\"}]}";
using (ClientWebSocket clientWebSocket = new ClientWebSocket())
{
try
{
await clientWebSocket.ConnectAsync(new Uri(baseUrl), CancellationToken.None);
await SendMessage(clientWebSocket, subscribeBooksRequest);
await ReceiveMessage(clientWebSocket);
await SendMessage(clientWebSocket, subscribeBooks5Request);
OrderBook orderBook = null;
ArrayPool<byte> pool = ArrayPool<byte>.Shared;
while (clientWebSocket.State == WebSocketState.Open)
{
string receivedMessage = await ReceiveMessage(clientWebSocket);
dynamic messageObject = JsonConvert.DeserializeObject(receivedMessage);
if (messageObject.action == "snapshot")
{
OrderBook originalOrderBook = OrderBook.ParseSnapshot(symbol, messageObject);
byte[] orderBookBytes = SerializeOrderBook(symbol, originalOrderBook, pool);
orderBook = DeserializeOrderBook(symbol, orderBookBytes);
}
else if (messageObject.action == "update")
{
if (orderBook != null)
{
OrderBookUpdate update = OrderBookUpdate.ParseUpdate(symbol, messageObject);
orderBook.LastTimeStamp = update.Timestamp;
orderBook.UpdateBook(update);
byte[] orderBookBytes = SerializeOrderBook(symbol, orderBook, pool);
orderBook = DeserializeOrderBook(symbol, orderBookBytes);
}
}
if (orderBook != null)
{
Log($"{symbol} mid: {orderBook.GetMidPrice()}");
}
}
}
catch (Exception ex)
{
Log($"An error occurred: {ex.Message} {ex.StackTrace}");
}
}
}
static async Task SendMessage(ClientWebSocket clientWebSocket, string message)
{
byte[] bytes = Encoding.UTF8.GetBytes(message);
await clientWebSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);
}
static async Task<string> ReceiveMessage(ClientWebSocket clientWebSocket)
{
byte[] buffer = new byte[1024];
WebSocketReceiveResult result;
StringBuilder message = new StringBuilder();
do
{
result = await clientWebSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
message.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
}
while (!result.EndOfMessage);
return message.ToString();
}
static byte[] SerializeOrderBook(string symbol, OrderBook orderBook, ArrayPool<byte> pool)
{
int size = CalculateSerializedSize(orderBook);
byte[] bytes = pool.Rent(size);
int offset = 0;
byte[] symbolBytes = Encoding.UTF8.GetBytes(orderBook.Symbol);
Buffer.BlockCopy(symbolBytes, 0, bytes, offset, symbolBytes.Length);
offset += symbolBytes.Length;
Buffer.BlockCopy(BitConverter.GetBytes(orderBook.LastTimeStamp), 0, bytes, offset, sizeof(long));
offset += sizeof(long);
// Serialize Bids
Buffer.BlockCopy(BitConverter.GetBytes(orderBook.Bids.Count), 0, bytes, offset, sizeof(int));
offset += sizeof(int);
foreach (var bid in orderBook.Bids)
{
Buffer.BlockCopy(BitConverter.GetBytes(bid.Price), 0, bytes, offset, sizeof(float));
offset += sizeof(float);
Buffer.BlockCopy(BitConverter.GetBytes(bid.Amount), 0, bytes, offset, sizeof(float));
offset += sizeof(float);
Buffer.BlockCopy(BitConverter.GetBytes(bid.NumOrders), 0, bytes, offset, sizeof(int));
offset += sizeof(int);
}
// Serialize Asks
Buffer.BlockCopy(BitConverter.GetBytes(orderBook.Asks.Count), 0, bytes, offset, sizeof(int));
offset += sizeof(int);
foreach (var ask in orderBook.Asks)
{
Buffer.BlockCopy(BitConverter.GetBytes(ask.Price), 0, bytes, offset, sizeof(float));
offset += sizeof(float);
Buffer.BlockCopy(BitConverter.GetBytes(ask.Amount), 0, bytes, offset, sizeof(float));
offset += sizeof(float);
Buffer.BlockCopy(BitConverter.GetBytes(ask.NumOrders), 0, bytes, offset, sizeof(int));
offset += sizeof(int);
}
return bytes;
}
static int CalculateSerializedSize(OrderBook orderBook)
{
return orderBook.Symbol.Length * 2 + // String: 2 bytes per char
sizeof(long) + // LastTimeStamp (8 bytes)
sizeof(int) + // Bids count (4 bytes)
sizeof(float) * 3 * orderBook.Bids.Count + // Bids: Price, Amount, NumOrders
sizeof(int) + // Asks count (4 bytes)
sizeof(float) * 3 * orderBook.Asks.Count; // Asks: Price, Amount, NumOrders
}
static OrderBook DeserializeOrderBook(string symbol, byte[] bytes)
{
int offset = 0;
string symbolStr = Encoding.UTF8.GetString(bytes, offset, symbol.Length);
offset += symbol.Length;
long lastTimeStamp = BitConverter.ToInt64(bytes, offset);
offset += sizeof(long);
OrderBook orderBook = new OrderBook(symbolStr, lastTimeStamp);
// Deserialize Bids
int numBids = BitConverter.ToInt32(bytes, offset);
offset += sizeof(int);
for (int i = 0; i < numBids; i++)
{
float price = BitConverter.ToSingle(bytes, offset);
offset += sizeof(float);
float amount = BitConverter.ToSingle(bytes, offset);
offset += sizeof(float);
int numOrders = BitConverter.ToInt32(bytes, offset);
offset += sizeof(int);
orderBook.Bids.Add(new Level { Price = price, Amount = amount, NumOrders = numOrders });
}
// Deserialize Asks
int numAsks = BitConverter.ToInt32(bytes, offset);
offset += sizeof(int);
for (int i = 0; i < numAsks; i++)
{
float price = BitConverter.ToSingle(bytes, offset);
offset += sizeof(float);
float amount = BitConverter.ToSingle(bytes, offset);
offset += sizeof(float);
int numOrders = BitConverter.ToInt32(bytes, offset);
offset += sizeof(int);
orderBook.Asks.Add(new Level { Price = price, Amount = amount, NumOrders = numOrders });
}
return orderBook;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment