Skip to content

WebSockets

The exchange pushes state changes over Phoenix channels. STX.Sdk wraps each topic in a strongly-typed class. Every channel handles:

  • Authenticating the connection, by token or by signing the handshake
  • Heartbeat and automatic reconnection
  • Rejoining its topic after a reconnect
Channel Topic What it pushes
STXPortfolioChannel portfolio:{user_id} Available balance, escrow, liabilities
STXActiveOrdersChannel active_orders:{user_id} Order state transitions
STXActiveTradesChannel active_trades:{user_id} Fills as they land
STXActiveSettlementsChannel active_settlements:{user_id} Settlements
STXPositionsChannel active_positions:{user_id} Position changes
STXUserInfoChannel user_info:{user_id} Account updates
STXMarketChannel market_info Market state and prices, broadcast rather than user-scoped

Register a callback, then start it:

var portfolio = serviceProvider.GetRequiredService<STXPortfolioChannel>();
portfolio.SetOnReceiveAction(p =>
Console.WriteLine($"Available: {p.AvailableBalance}c Escrow: {p.Escrow}c"));
await portfolio.StartAsync();

StartAsync opens the socket, joins the topic, and begins the heartbeat. StopAsync closes it. WebSocketConnected reports the current state, and the SocketDisconnected and SocketReconnected events fire around an automatic reconnect.

Composition: hand the channel a callback, as above. Best when you want each message as it lands.

Inheritance: subclass and override OnReceive:

public class BalanceTracker : STXPortfolioChannel
{
public BalanceTracker(STXUserStorage storage, STXEndpointSettings settings)
: base(storage, settings) { }
public override void OnReceive(STXPortfolio portfolio)
{
AvailableBalance = portfolio.AvailableBalance;
}
public long AvailableBalance { get; private set; }
}

Wrappers: every channel also has a …ChannelWrapper registered alongside it, which buffers into a bounded queue instead of calling you back. Use it when you would rather poll than handle a callback:

var wrapper = serviceProvider.GetRequiredService<STXPortfolioChannelWrapper>();
await wrapper.StartAsync();
var latest = wrapper.LastItem; // most recent message, or null
var all = wrapper.Items; // buffered messages
var orders = serviceProvider.GetRequiredService<STXActiveOrdersChannel>();
var trades = serviceProvider.GetRequiredService<STXActiveTradesChannel>();
orders.SetOnReceiveAction(o =>
{
foreach (var order in o.Orders)
Console.WriteLine($"{order.Id} {order.Status} {order.Filled}/{order.Quantity}");
});
trades.SetOnReceiveAction(t =>
{
foreach (var trade in t.Trades)
Console.WriteLine($"Filled {trade.MarketId} at {trade.Price}c");
});
await orders.StartAsync();
await trades.StartAsync();

Market info is a broadcast, so it is not keyed on a user and works without GetMeAsync():

var market = serviceProvider.GetRequiredService<STXMarketChannel>();
market.SetOnReceiveAction(m =>
Console.WriteLine($"{m.MarketId} {m.Status}"));
await market.StartAsync();
public class STXWorker : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stop)
{
// Channel topics are keyed on the user id. Under API-key auth there is no
// login response, so fetch it once before joining anything.
await _identity.GetMeAsync();
_portfolio.SetOnReceiveAction(p => _balance = p.AvailableBalance); // long, in cents
_orders.SetOnReceiveAction(HandleOrders);
await _portfolio.StartAsync();
await _orders.StartAsync();
await Task.Delay(Timeout.Infinite, stop);
}
public override async Task StopAsync(CancellationToken stop)
{
await _orders.StopAsync();
await _portfolio.StopAsync();
await base.StopAsync(stop);
}
}

Channels are registered as singletons, so the same instance is shared across your app. Resolve them once and keep them.

  • Trading: placing the orders these channels report on
  • Settlements: settlement history and its channel
v1.5.9Changelogllms.txtllms-full.txt