Created
March 28, 2024 05:07
-
-
Save normanlmfung/7bd4e9d00322d65f185d5804d5606229 to your computer and use it in GitHub Desktop.
csharp_ws
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.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