Skip to content

Combos

A combination is one market over two or more legs. As a player you ask for quotes on a set of legs and a stake; makers quote what you win; you accept, and the exchange places one ordinary order for each side on the combination’s market. Combos explains the product and Combo negotiation documents every command, push and refusal at the wire level. This page shows the SDK calls.

Both sides run on the socket, with an API key that has write access. Every push arrives as an STXChannelMessage whose Data is a typed model from STX.Sdk.Combos, chosen by the message’s Event. Money is a dollar string, and ExpiresAt is Unix microseconds.

using STX.Sdk;
using STX.Sdk.Combos;
using var client = new STXClient();
await using var ws = client.CreateWebSocket();
var rfq = await ws.RfqAsync(onMessage: msg =>
{
switch (msg.Data)
{
case BestQuote best: Console.WriteLine($"best {best.QuoteId} {best.Amount}"); break;
case ComboPlacement placed: Console.WriteLine($"placed {placed.OrderId} {placed.Quantity} @ {placed.Price}"); break;
case NegotiationClosed closed: Console.WriteLine($"closed {closed.Reason}"); break;
}
});
Console.WriteLine(rfq.JoinReply.ExternalId); // your pseudonym, as makers see it
var request = await rfq.RequestQuoteAsync(
[new ComboLeg(legA, "won"), new ComboLeg(legB, "won")],
amount: "25.00");
Console.WriteLine(request.RfqId);

RequestQuoteAsync asks makers to quote on the legs for a stake (dollars, in whole cents). The request lives five minutes and belongs to the channel that opened it: leaving the channel or losing the connection cancels it.

When a best quote you like arrives, accept it:

await foreach (var msg in rfq.ReadAllAsync())
{
if (msg.Data is not BestQuote { QuoteId: not null } best || best.RfqId != request.RfqId) continue;
try
{
var placement = await rfq.AcceptAsync(request.RfqId!, best.QuoteId!, best.Amount!);
Console.WriteLine($"{placement.OrderId} {placement.Quantity} @ {placement.Price}");
break;
}
catch (STXComboPriceMovedException ex)
{
// The best quote fell below what you accepted; the request stays open.
Console.WriteLine($"now {ex.BestQuote.Amount}");
}
}

The best live quote is placed when its amount is at least the one you accepted, so the result can be better than the quote you saw. The exchange can take up to 30 seconds to answer. Fills, positions and settlements of the placed order arrive on the usual account channels.

SnapshotAsync(rfqId) returns the request as it stands now with the live best quote, CancelAsync(rfqId) ends it, and CashOutAsync(marketId) asks makers to quote on what you hold of a combo market.

var maker = await ws.ComboQuoteAsync();
await foreach (var msg in maker.ReadAllAsync())
{
if (msg.Data is RfqOpened { Side: "buy" } opened)
{
// Price against MaxPrice and TickSize, not a constant.
var quote = await maker.QuoteAsync(opened.RfqId!, amount: "180.00", expiresInMs: 30_000, clientOrderId: "mm-0417");
Console.WriteLine($"quoted {quote.QuoteId}");
}
}

Every request opened after the join arrives as RfqOpened; earlier ones are not replayed. QuoteAsync quotes what the player wins on a buy, or what you pay for the whole size on a cash out. WithdrawQuoteAsync(rfqId, quoteId) withdraws a quote; to change one, withdraw it and quote again. A quote belongs to the channel that placed it. Pushes: RfqOpened, RfqClosed, QuoteAccepted, MakerPlacement (placed) and NegotiationClosed.

A complete maker that quotes every request until Ctrl+C and reports how each one ends is combo-maker; Quote combos as a maker walks through it.

A refused command throws STXComboException with the exchange’s reason in ComboReason and the command in Command. A reason with a detail, such as player_refused:loss_limit24_hours, is split: ComboReason is player_refused and Detail is loss_limit24_hours. A refused join is final for that socket: the channel is not rejoined after a reconnect.