Created
April 1, 2024 05:27
-
-
Save normanlmfung/5bbd2420251b29150db90aca3e139d3c to your computer and use it in GitHub Desktop.
csharp_ws_arraypool
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
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