Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save normanlmfung/7bd4e9d00322d65f185d5804d5606229 to your computer and use it in GitHub Desktop.
Save normanlmfung/7bd4e9d00322d65f185d5804d5606229 to your computer and use it in GitHub Desktop.
csharp_ws
using Newtonsoft.Json;
using System;
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
*/
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
class Level(float price, float amount, int numOrders)
{
public float Price { get; set; } = price;
public float Amount { get; set; } = amount;
public int NumOrders { get; set; } = numOrders;
}
class OrderBookUpdate(string symbol)
{
public string Symbol { get; } = symbol;
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 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;
/* In a real trading system, you need check:
a. Timestamp from ws updates vs local clock
b. Timestamp from ws updates vs previous updates, check if latency issue
*/
return update;
}
private static List<Level> ParseLevels(dynamic levels)
{
List<Level> parsedLevels = new List<Level>();
foreach (var level in levels)
{
parsedLevels.Add(new Level(float.Parse(level[0].ToString()), float.Parse(level[1].ToString()), int.Parse(level[3].ToString()) ));
}
return parsedLevels;
}
}
class OrderBook(string symbol, long lastTimestamp)
{
public string Symbol => symbol.Trim();
public long LastTimeStamp { get; set; } = lastTimestamp;
public List<Level> Bids { get; set; } = new List<Level>();
public List<Level> Asks { get; set; } = 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(float.Parse(ask[0].ToString()), float.Parse(ask[1].ToString()), int.Parse(ask[3].ToString()) ));
}
foreach (var bid in messageObject.data[0].bids)
{
orderBook.Bids.Add(new Level(float.Parse(bid[0].ToString()), float.Parse(bid[1].ToString()), 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
{
// Connect to WebSocket
await clientWebSocket.ConnectAsync(new Uri(baseUrl), CancellationToken.None);
// Subscribe to 'books' for initial order book snapshot
await SendMessage(clientWebSocket, subscribeBooksRequest);
// Ignore the first response from OKX
await ReceiveMessage(clientWebSocket);
// Subscribe to 'books5' for incremental updates
await SendMessage(clientWebSocket, subscribeBooks5Request);
OrderBook orderBook = null;
while (clientWebSocket.State == WebSocketState.Open)
{
// Receive message from WebSocket
string receivedMessage = await ReceiveMessage(clientWebSocket);
// Deserialize JSON to dynamic object
dynamic messageObject = JsonConvert.DeserializeObject(receivedMessage);
// Check if it's an initial snapshot or an update
if (messageObject.action == "snapshot")
{
orderBook = OrderBook.ParseSnapshot(symbol, messageObject);
}
else if (messageObject.action == "update")
{
if (orderBook != null)
{
OrderBookUpdate update = OrderBookUpdate.ParseUpdate(symbol, messageObject);
orderBook.LastTimeStamp = update.Timestamp;
orderBook.UpdateBook(update);
}
}
if (orderBook!=null) {
Log($"{symbol} mid: {orderBook.GetMidPrice()}");
}
}
}
catch (Exception ex)
{
Log($"An error occurred: {ex.Message}");
}
}
}
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();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment