WebSocket API Documentation
Real-time data streaming for user positions, orders, trade executions, balance updates, and public market data via SignalR WebSocket hubs.
Key Features
- Private user data: Positions, orders, trades, and balance updates pushed in real-time
- Public market data: OrderBook snapshots & deltas, mini tickers, mark prices
- Authentication: API Key + HMAC-SHA256 signature
- Per-user isolation: Private events delivered exclusively to authenticated user channel
- Push-based snapshots: Market data subscribers receive full snapshot on subscribe, then continuous deltas
- Sequence-based gap recovery: OrderBook updates include sequence IDs for gap detection and replay
Hub Endpoints
| Hub | Endpoint | Transport | Purpose |
|---|---|---|---|
| GlobalHub | /GlobalHub |
WebSocket | Private user events: positions, orders, trades, balance, notifications |
| PublicMarketData | /PublicMarketData |
WebSocket | OrderBook streaming, mini tickers, mark prices |
1. Authentication
Authentication is mandatory for receiving private events. Unauthenticated connections receive no user data. Authenticate using your API Key + HMAC-SHA256 signature — the same API Key credentials as the REST Trade API. The server sends LR (Login Request) immediately on connect; respond by signing the current timestamp with HMAC-SHA256 using your API Secret.
import hmac, hashlib, time
def on_login_request():
timestamp = int(time.time() * 1000)
signature = hmac.new(
API_SECRET.encode("utf-8"),
str(timestamp).encode("utf-8"),
hashlib.sha256
).hexdigest()
hub.send("WebSocketLoginWithApiKey", [API_KEY, signature, timestamp])
hub.on("LR", on_login_request)const crypto = require("crypto");
hub.on("LR", () => {
const timestamp = Date.now();
const signature = crypto
.createHmac("sha256", API_SECRET)
.update(timestamp.toString())
.digest("hex");
hub.invoke("WebSocketLoginWithApiKey", API_KEY, signature, timestamp);
});using System.Security.Cryptography;
using System.Text;
hub.On("LR", async () =>
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(API_SECRET));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(timestamp.ToString()));
var signature = Convert.ToHexStringLower(hash);
await hub.InvokeAsync("WebSocketLoginWithApiKey", API_KEY, signature, timestamp);
});| Parameter | Type | Description |
|---|---|---|
apiKey | string | API Key from account dashboard |
signature | string | HMAC-SHA256(apiSecret, timestamp.toString()), lowercase hex |
timestamp | long | Unix milliseconds. Must be within 5 seconds of server time. |
Connection & Authentication Flow
2. Private Events (GlobalHub)
All private events are pushed to authenticated users in real-time. No subscription needed — events are delivered automatically after login.
FuturesAccountUpdate
Pushed when any futures position or account state changes: position open/close, size change, PnL recalculation, margin adjustments, liquidation price update.
{
"positions": [
{
"positionId": 12345,
"symbol": "BTCUSDT",
"baseAsset": "BTC",
"quoteAsset": "USDT",
"size": 0.5,
"orderSide": "Buy",
"marginMode": "Isolated",
"entryPrice": 65230.50,
"quantity": 0.5,
"leverage": 10,
"initialMargin": 3261.525,
"marginBalance": 3350.00,
"maintenanceMargin": 163.08,
"liquidationPrice": 58700.00,
"unrealizedPnL": 125.75,
"realizedPnL": 0,
"markPrice": 65482.00,
"fundingFee": -1.23,
"adl": 2,
"openedAt": "2025-03-15T10:30:00Z"
}
],
"symbol": "BTCUSDT",
"availableBalance": 15000.00,
"maintenanceMargin": 163.08,
"marginBalance": 3350.00,
"balance": 18350.00,
"accountequity": 18475.75,
"unrealizedPnL": 125.75,
"walletMode": "OneWay",
"marginCallRate": 20,
"marginRatio": 0.048
}OrderUpdate
Pushed on every order state change: creation, partial fill, full fill, cancellation. A single event stream covers both active orders and order history.
{
"orderId": 98765,
"clientOrderId": "my-order-001",
"symbol": "BTCUSDT",
"orderBookType": "Futures",
"side": "Buy",
"type": "Limit",
"orderDirection": "Open",
"baseAsset": "BTC",
"quoteAsset": "USDT",
"price": 65000.00,
"quantity": 0.5,
"executedQuantity": 0.3,
"averagePrice": 65010.25,
"cost": 19503.075,
"status": "PartiallyFilled",
"created": "2025-03-15T10:30:00Z",
"updated": "2025-03-15T10:30:05Z",
"stopTriggered": false,
"stopPrice": 0,
"stopLossTriggerPrice": 60000.00,
"takeProfitTriggerPrice": 70000.00,
"source": "Api"
}Order Status Values
| Status | Description |
|---|---|
New | Order placed, waiting in order book |
PartiallyFilled | Partially executed, remaining quantity still active |
Filled | Fully executed |
Canceled | Canceled by user or system |
TradeUpdate
Pushed on each trade execution (fill). Each partial fill generates a separate event. Contains maker/taker information via the isBuyerMaker field.
{
"id": 456789,
"symbol": "BTCUSDT",
"price": 65010.25,
"quantity": 0.3,
"quoteQuantity": 19503.075,
"orderSide": "Buy",
"isBuyerMaker": false,
"isBestMatch": true,
"time": "2025-03-15T10:30:05Z"
}Maker / Taker Determination
The isBuyerMaker boolean indicates whether the buyer's order was the maker (already resting on the book). To determine your liquidity role:
def on_trade_update(args):
trade = args[0]
is_maker = (trade["orderSide"] == "Buy" and trade["isBuyerMaker"]) or \
(trade["orderSide"] == "Sell" and not trade["isBuyerMaker"])
print(f"Trade {trade['id']}: {trade['quantity']} @ {trade['price']}")
print("Liquidity: " + ("MAKER" if is_maker else "TAKER"))
hub.on("TradeUpdate", on_trade_update)hub.on("TradeUpdate", (trade) => {
const isMaker = (trade.orderSide === "Buy" && trade.isBuyerMaker) ||
(trade.orderSide === "Sell" && !trade.isBuyerMaker);
console.log(`Trade ${trade.id}: ${trade.quantity} @ ${trade.price}`);
console.log(`Liquidity: ${isMaker ? "MAKER" : "TAKER"}`);
});hub.On<TradeItem>("TradeUpdate", trade =>
{
var isMaker = (trade.OrderSide == "Buy" && trade.IsBuyerMaker) ||
(trade.OrderSide == "Sell" && !trade.IsBuyerMaker);
Console.WriteLine($"Trade {trade.Id}: {trade.Quantity} @ {trade.Price}");
Console.WriteLine($"Liquidity: {(isMaker ? "MAKER" : "TAKER")}");
});BalanceUpdate
Pushed when the user's wallet balance changes: after trades, deposits, withdrawals, margin adjustments, funding fee settlements.
{
"userId": 12345,
"type": "Futures",
"currencyId": 1,
"symbol": "USDT",
"currencyName": "Tether",
"free": 15000.00,
"locked": 3261.525,
"total": 18261.525,
"block": 0
}Additional Private Events
| Event | Payload | Description |
|---|---|---|
AccountUpdate | FuturesAccount | Futures account configuration change (wallet mode, margin call rate) |
Notification | GlobalNotificationMessage | System alerts and announcements |
DepositCryptoUpdate | DepositCryptoResponse | Crypto deposit confirmed |
WithdrawCryptoUpdate | WithdrawCryptoResponse | Crypto withdrawal status change |
KYCUpdate | KYCStatusUpdate | KYC verification status change |
3. Market Data (PublicMarketData Hub)
Public market data is served via the /PublicMarketData hub. All market data streams use push-based snapshots: on subscription the server sends a full snapshot immediately, followed by continuous delta updates.
OrderBook Stream
Subscribe to an order book to receive a full snapshot and real-time delta updates every 250ms.
last_seq_id = 0
market_hub.send("Subscribe", ["socket.darkex.com", "BTCUSDT", "Spot", 500])
def on_snapshot(args):
global last_seq_id
msg = args[0] # msg["s"]=seq, msg["b"]=bids, msg["a"]=asks, msg["tr"]=trades
init_order_book(msg)
last_seq_id = msg["s"]
def on_update(args):
global last_seq_id
msg = args[0]
if msg["s"] != last_seq_id + 1: # gap detected - request replay
market_hub.send("RequestReplay", ["socket.darkex.com", "BTCUSDT", "Spot", last_seq_id])
return
apply_delta(msg) # qty "0" = remove level, non-zero = replace
last_seq_id = msg["s"]
market_hub.on("OrderBookSnapshot", on_snapshot)
market_hub.on("OrderBookUpdate", on_update)// Subscribe
await marketHub.invoke("Subscribe", "socket.darkex.com", "BTCUSDT", "Spot", 500);
// Snapshot (immediate, on subscribe)
marketHub.on("OrderBookSnapshot", (msg) => {
// msg.s = sequence ID, msg.b = bids, msg.a = asks, msg.tr = recent trades
initOrderBook(msg);
lastSeqId = msg.s;
});
// Delta updates (every 250ms)
marketHub.on("OrderBookUpdate", (msg) => {
if (msg.s !== lastSeqId + 1) {
// Gap detected - request replay
marketHub.invoke("RequestReplay", "socket.darkex.com", "BTCUSDT", "Spot", lastSeqId);
return;
}
applyDelta(msg); // qty "0" = remove level, non-zero = replace
lastSeqId = msg.s;
});long lastSeqId = 0;
await marketHub.InvokeAsync("Subscribe", "socket.darkex.com", "BTCUSDT", "Spot", 500);
// Snapshot (immediate, on subscribe)
marketHub.On<OrderBookMessage>("OrderBookSnapshot", msg =>
{
InitOrderBook(msg); // msg.S = sequence, msg.B = bids, msg.A = asks, msg.Tr = trades
lastSeqId = msg.S;
});
// Delta updates (every 250ms)
marketHub.On<OrderBookMessage>("OrderBookUpdate", msg =>
{
if (msg.S != lastSeqId + 1) // gap detected - request replay
{
marketHub.InvokeAsync("RequestReplay", "socket.darkex.com", "BTCUSDT", "Spot", lastSeqId);
return;
}
ApplyDelta(msg); // qty "0" = remove level, non-zero = replace
lastSeqId = msg.S;
});OrderBook Snapshot Wire Format
{
"c": "snapshot",
"s": 42,
"t": 1709500000000,
"p": "BTCUSDT",
"o": "spot",
"d": 500,
"b": [["65230.50", "1.234"], ["65230.00", "0.567"]],
"a": [["65231.00", "0.890"], ["65231.50", "2.345"]],
"tr": [{"i": 1001, "p": "65230.75", "q": "0.100", "m": true, "t": 1709499999500}]
}OrderBook Update Wire Format
{
"c": "update",
"s": 43,
"t": 1709500000250,
"p": "BTCUSDT",
"o": "spot",
"b": [["65230.50", "1.500"], ["65229.00", "0"]],
"a": [["65231.00", "0.500"]],
"tr": [{"i": 1002, "p": "65230.50", "q": "0.050", "m": false, "t": 1709500000100}]
}| Field | Type | Description |
|---|---|---|
c | string | Message type: "snapshot" or "update" |
s | long | Sequence ID (continuous per order book, for gap detection) |
t | long | Server timestamp (Unix ms) |
p | string | Pair symbol |
o | string | Order book type: "spot" or "futures" |
d | int | Depth level (snapshot only) |
b | array | Bids: [[price, qty], ...] |
a | array | Asks: [[price, qty], ...] |
tr | array | Recent trades |
"0" means remove the price level. Non-zero quantity means replace the entire quantity at that level. Sequence gaps require RequestReplay.
Depth Levels
Available depth levels: 50, 100, 500 (default), 1000
Gap Recovery
The server keeps the last 20 updates (~5 seconds) per order book in a ring buffer. On gap detection, call RequestReplay(domain, symbol, type, lastSeqId). If the gap is within the buffer, individual deltas are replayed. Otherwise a fresh snapshot is sent.
Ticker Stream
Subscribe to receive a full ticker snapshot for all pairs in a tenant, followed by delta updates every 500ms (only changed tickers).
tickers = {}
market_hub.send("SubscribeMiniTickers", ["socket.darkex.com"])
def on_ticker_snapshot(args):
for t in args[0]["d"]: # full snapshot
tickers[t["s"]] = t
def on_ticker_update(args):
for t in args[0]["d"]: # changed tickers only
tickers[t["s"]] = {**tickers.get(t["s"], {}), **t}
market_hub.on("TickerSnapshot", on_ticker_snapshot)
market_hub.on("TickerUpdate", on_ticker_update)await marketHub.invoke("SubscribeMiniTickers", "socket.darkex.com");
// Full snapshot (immediate)
marketHub.on("TickerSnapshot", (msg) => {
// msg.d = array of all tickers
msg.d.forEach(t => tickers.set(t.s, t));
});
// Delta updates (every 500ms, changed tickers only)
marketHub.on("TickerUpdate", (msg) => {
msg.d.forEach(t => tickers.set(t.s, { ...tickers.get(t.s), ...t }));
});var tickers = new Dictionary<string, Ticker>();
await marketHub.InvokeAsync("SubscribeMiniTickers", "socket.darkex.com");
// Full snapshot (immediate)
marketHub.On<TickerMessage>("TickerSnapshot", msg =>
{
foreach (var t in msg.D) tickers[t.S] = t;
});
// Delta updates (every 500ms, changed tickers only)
marketHub.On<TickerMessage>("TickerUpdate", msg =>
{
foreach (var t in msg.D) tickers[t.S] = t; // merge changed fields into your cached ticker
});Ticker Fields
| Field | Type | Description |
|---|---|---|
s | string | Symbol (e.g. BTCUSDT) |
o | string | Order book type: spot or futures |
p | string | Last price |
op | string | Open price (24h) |
h | string | High price (24h) |
l | string | Low price (24h) |
v | string | Base volume (24h) |
q | string | Quote volume (24h) |
c | int | Trade count (24h) |
m | string | Mark price (futures only) |
f | string | Funding rate (futures only) |
Mark Price Stream
Mark price updates are automatically subscribed when you subscribe to a Futures order book. Delivered every 1000ms.
{
"c": "markprice_update",
"t": 1709500000000,
"d": [
{ "s": "BTCUSDT", "m": "65235.50", "i": "65234.80", "f": "0.00010", "t": 1709503600000 }
]
}| Field | Description |
|---|---|
s | Symbol |
m | Mark price |
i | Index price |
f | Funding rate |
t | Next funding timestamp |
4. Data Schema Reference
Position (PositionView)
Each position in the FuturesAccountUpdate.positions[] array:
| Field | Type | Description |
|---|---|---|
positionId | long | Unique position identifier |
symbol | string | Trading pair (e.g. BTCUSDT) |
baseAsset | string | Base asset (e.g. BTC) |
quoteAsset | string | Quote asset (e.g. USDT) |
size | decimal | Contract size |
orderSide | string | Buy (Long) or Sell (Short) |
marginMode | string | Isolated or Cross |
entryPrice | decimal | Average entry price |
quantity | decimal | Current open quantity |
leverage | decimal | Position leverage |
initialMargin | decimal | Initial margin amount |
marginBalance | decimal | Current margin balance |
maintenanceMargin | decimal | Maintenance margin requirement |
liquidationPrice | decimal | Estimated liquidation price |
unrealizedPnL | decimal | Unrealized profit/loss |
realizedPnL | decimal | Realized profit/loss |
markPrice | decimal | Current mark price |
fundingFee | decimal | Accumulated funding fees |
adl | int | Auto-deleverage indicator (0-5) |
marginRatio | decimal | Margin ratio percentage |
openedAt | datetime | Position open timestamp |
Order (OrderDetailResponse)
| Field | Type | Description |
|---|---|---|
orderId | long | Unique order identifier |
clientOrderId | string? | Client-assigned order ID |
symbol | string | Trading pair |
orderBookType | string | Spot or Futures |
side | string | Buy or Sell |
type | string | Limit, Market, StopLimit, StopMarket |
orderDirection | string | Open or Close |
price | decimal? | Limit price (null for market orders) |
quantity | decimal? | Original order quantity |
executedQuantity | decimal | Filled quantity |
averagePrice | decimal | Average fill price |
cost | decimal | Total cost |
status | string | New, PartiallyFilled, Filled, Canceled |
created | datetime | Order creation timestamp |
updated | datetime? | Last update timestamp |
stopPrice | decimal | Stop trigger price |
stopLossTriggerPrice | decimal | Stop-loss trigger price |
takeProfitTriggerPrice | decimal | Take-profit trigger price |
source | string | Order source: Web, Api, Mobile |
Trade (TradeItem)
| Field | Type | Description |
|---|---|---|
id | long | Trade ID |
symbol | string | Trading pair |
price | decimal | Execution price |
quantity | decimal | Executed quantity |
quoteQuantity | decimal | Quote asset amount (price * quantity) |
orderSide | string | Buy or Sell |
isBuyerMaker | boolean | Whether the buyer was the maker (resting order) |
isBestMatch | boolean | Best match indicator |
time | datetime | Execution timestamp |
Balance (ResponseWallet)
| Field | Type | Description |
|---|---|---|
userId | long | User identifier |
type | string | Wallet type: Spot or Futures |
currencyId | long | Currency identifier |
symbol | string | Currency symbol (e.g. USDT) |
currencyName | string | Currency display name |
free | decimal | Available balance |
locked | decimal | In-trade / margin locked amount |
total | decimal | Total balance (free + locked) |
block | decimal | Blocked amount (withdrawal hold etc.) |
5. Integration Guide
Recommended Integration Flow
- Connect both hubs (
/GlobalHubfor private data,/PublicMarketDatafor market data) - Register event handlers before authentication (to avoid missing events during REST fetch)
- Authenticate on GlobalHub (API Key + HMAC)
- Fetch initial state via REST API (positions, orders, balance)
- Subscribe to market data streams on PublicMarketData hub (snapshot delivered automatically)
- Apply deltas from WebSocket events to local state
Complete Integration Example
import hmac, hashlib, time
from signalrcore.hub_connection_builder import HubConnectionBuilder
API_KEY = "your-api-key"
API_SECRET = "your-api-secret"
DOMAIN = "socket.darkex.com"
# === State ===
positions, open_orders, balances = {}, {}, {}
last_seq_id = 0
# === GlobalHub (private data) ===
global_hub = HubConnectionBuilder() \
.with_url("wss://socket.darkex.com/GlobalHub") \
.with_automatic_reconnect({"type": "raw", "keep_alive_interval": 15, "reconnect_interval": 5}) \
.build()
# === PublicMarketData hub ===
market_hub = HubConnectionBuilder() \
.with_url("wss://socket.darkex.com/PublicMarketData") \
.with_automatic_reconnect({"type": "raw", "keep_alive_interval": 15, "reconnect_interval": 5}) \
.build()
# --- Private event handlers ---
def on_account(args):
for pos in args[0]["positions"]:
if pos["quantity"] > 0:
positions[pos["positionId"]] = pos
else:
positions.pop(pos["positionId"], None)
def on_order(args):
order = args[0]
if order["status"] in ("New", "PartiallyFilled"):
open_orders[order["orderId"]] = order
else:
open_orders.pop(order["orderId"], None)
def on_balance(args):
w = args[0]
balances[f"{w['type']}:{w['symbol']}"] = w
global_hub.on("FuturesAccountUpdate", on_account)
global_hub.on("OrderUpdate", on_order)
global_hub.on("BalanceUpdate", on_balance)
# --- Authentication ---
def on_login_request():
timestamp = int(time.time() * 1000)
signature = hmac.new(API_SECRET.encode(), str(timestamp).encode(), hashlib.sha256).hexdigest()
global_hub.send("WebSocketLoginWithApiKey", [API_KEY, signature, timestamp])
global_hub.on("LR", on_login_request)
global_hub.on("LS", lambda _: print("Authenticated")) # then fetch initial state via REST
# --- Market data handlers ---
def on_snapshot(args):
global last_seq_id
init_order_book(args[0]); last_seq_id = args[0]["s"]
def on_update(args):
global last_seq_id
msg = args[0]
if msg["s"] != last_seq_id + 1:
market_hub.send("RequestReplay", [DOMAIN, "BTCUSDT", "Spot", last_seq_id]); return
apply_delta(msg); last_seq_id = msg["s"]
market_hub.on("OrderBookSnapshot", on_snapshot)
market_hub.on("OrderBookUpdate", on_update)
# --- Start ---
global_hub.start()
market_hub.start()
# Subscribe to market data (snapshot pushed automatically)
market_hub.send("Subscribe", [DOMAIN, "BTCUSDT", "Spot", 500])
market_hub.send("SubscribeMiniTickers", [DOMAIN])import * as signalR from "@microsoft/signalr";
const crypto = require("crypto");
const API_KEY = "your-api-key";
const API_SECRET = "your-api-secret";
const DOMAIN = "socket.darkex.com";
// === State ===
const positions = new Map();
const openOrders = new Map();
const balances = new Map();
// === GlobalHub (Private Data) ===
const globalHub = new signalR.HubConnectionBuilder()
.withUrl("wss://socket.darkex.com/GlobalHub", {
transport: signalR.HttpTransportType.WebSockets,
skipNegotiation: true
})
.withAutomaticReconnect([0, 1000, 5000, 10000])
.build();
// === PublicMarketData Hub ===
const marketHub = new signalR.HubConnectionBuilder()
.withUrl("wss://socket.darkex.com/PublicMarketData", {
transport: signalR.HttpTransportType.WebSockets,
skipNegotiation: true
})
.withAutomaticReconnect([0, 1000, 5000, 10000])
.build();
// --- Private Event Handlers ---
globalHub.on("FuturesAccountUpdate", (account) => {
account.positions.forEach(pos => {
pos.quantity > 0
? positions.set(pos.positionId, pos)
: positions.delete(pos.positionId);
});
});
globalHub.on("OrderUpdate", (order) => {
if (order.status === "New" || order.status === "PartiallyFilled") {
openOrders.set(order.orderId, order);
} else {
openOrders.delete(order.orderId);
}
});
globalHub.on("TradeUpdate", (trade) => {
const isMaker = (trade.orderSide === "Buy" && trade.isBuyerMaker) ||
(trade.orderSide === "Sell" && !trade.isBuyerMaker);
console.log(`Trade: ${trade.symbol} ${trade.quantity} @ ${trade.price} [${isMaker ? "MAKER" : "TAKER"}]`);
});
globalHub.on("BalanceUpdate", (wallet) => {
balances.set(`${wallet.type}:${wallet.symbol}`, wallet);
});
// --- Authentication ---
globalHub.on("LR", () => {
const timestamp = Date.now();
const signature = crypto
.createHmac("sha256", API_SECRET)
.update(timestamp.toString())
.digest("hex");
globalHub.invoke("WebSocketLoginWithApiKey", API_KEY, signature, timestamp);
});
globalHub.on("LS", async () => {
console.log("Authenticated");
// Fetch initial state via REST
const [pos, ord, bal] = await Promise.all([
fetch("/fapi/v1/positionRisk", { headers: authHeaders }).then(r => r.json()),
fetch("/fapi/v1/openOrders", { headers: authHeaders }).then(r => r.json()),
fetch("/fapi/v1/balance", { headers: authHeaders }).then(r => r.json())
]);
pos.forEach(p => positions.set(p.positionId, p));
ord.forEach(o => openOrders.set(o.orderId, o));
bal.forEach(b => balances.set(`${b.type}:${b.symbol}`, b));
console.log("State initialized");
});
// --- Market Data Handlers ---
let lastSeqId = 0;
marketHub.on("OrderBookSnapshot", (msg) => {
initOrderBook(msg);
lastSeqId = msg.s;
});
marketHub.on("OrderBookUpdate", (msg) => {
if (msg.s !== lastSeqId + 1) {
marketHub.invoke("RequestReplay", DOMAIN, "BTCUSDT", "Spot", lastSeqId);
return;
}
applyDelta(msg);
lastSeqId = msg.s;
});
// --- Start ---
await Promise.all([globalHub.start(), marketHub.start()]);
// Subscribe to market data (snapshot is pushed automatically)
await marketHub.invoke("Subscribe", DOMAIN, "BTCUSDT", "Spot", 500);
await marketHub.invoke("SubscribeMiniTickers", DOMAIN);using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.SignalR.Client;
const string ApiKey = "your-api-key";
const string ApiSecret = "your-api-secret";
const string Domain = "socket.darkex.com";
// === State ===
var positions = new Dictionary<long, PositionView>();
var openOrders = new Dictionary<long, OrderDetailResponse>();
var balances = new Dictionary<string, ResponseWallet>();
long lastSeqId = 0;
// === GlobalHub (private data) ===
var globalHub = new HubConnectionBuilder()
.WithUrl("wss://socket.darkex.com/GlobalHub", o =>
{
o.Transports = HttpTransportType.WebSockets;
o.SkipNegotiation = true;
})
.WithAutomaticReconnect()
.Build();
// === PublicMarketData hub ===
var marketHub = new HubConnectionBuilder()
.WithUrl("wss://socket.darkex.com/PublicMarketData", o =>
{
o.Transports = HttpTransportType.WebSockets;
o.SkipNegotiation = true;
})
.WithAutomaticReconnect()
.Build();
// --- Private event handlers ---
globalHub.On<FuturesAccountView>("FuturesAccountUpdate", account =>
{
foreach (var pos in account.Positions)
{
if (pos.Quantity > 0) positions[pos.PositionId] = pos;
else positions.Remove(pos.PositionId);
}
});
globalHub.On<OrderDetailResponse>("OrderUpdate", order =>
{
if (order.Status is "New" or "PartiallyFilled") openOrders[order.OrderId] = order;
else openOrders.Remove(order.OrderId);
});
globalHub.On<ResponseWallet>("BalanceUpdate", wallet =>
{
balances[$"{wallet.Type}:{wallet.Symbol}"] = wallet;
});
// --- Authentication ---
globalHub.On("LR", async () =>
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(ApiSecret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(timestamp.ToString()));
var signature = Convert.ToHexStringLower(hash);
await globalHub.InvokeAsync("WebSocketLoginWithApiKey", ApiKey, signature, timestamp);
});
globalHub.On("LS", () => Console.WriteLine("Authenticated")); // then fetch initial state via REST
// --- Market data handlers ---
marketHub.On<OrderBookMessage>("OrderBookSnapshot", msg =>
{
InitOrderBook(msg);
lastSeqId = msg.S;
});
marketHub.On<OrderBookMessage>("OrderBookUpdate", msg =>
{
if (msg.S != lastSeqId + 1)
{
marketHub.InvokeAsync("RequestReplay", Domain, "BTCUSDT", "Spot", lastSeqId);
return;
}
ApplyDelta(msg);
lastSeqId = msg.S;
});
// --- Start ---
await globalHub.StartAsync();
await marketHub.StartAsync();
// Subscribe to market data (snapshot pushed automatically)
await marketHub.InvokeAsync("Subscribe", Domain, "BTCUSDT", "Spot", 500);
await marketHub.InvokeAsync("SubscribeMiniTickers", Domain);