Trading providers
Overview
The execution side of the platform: a trading provider hosts accounts, accepts orders, streams every state change as an event, and the trading service aggregates them all into one unified view.
On this page
What a trading provider is
Market data (bars, ticks, depth) is modelled by IDataProvider and covered in the
Venue plugins chapter. Order routing is a separate concern
with its own contract, ITradingProvider (namespace SabrTrader.Pipeline.Venues.Trading).
A trading provider owns a set of accounts under one ProviderKey, accepts order placement,
cancellation and modification, and pushes out a stream of TradingEvents as state evolves.
The built-in simulator, the futures and forex brokers (Rithmic, OANDA, Interactive Brokers) and the crypto venues are all
implementations of this one interface. A real broker connection typically implements both an
IDataProvider (prices) and an ITradingProvider (execution), sharing the same
provider key so accounts and instruments line up.
ITradingProvider directly.
It is published by a venue session as its Trading port: the host's connection layer builds
the session, starts the provider, and hands it to the platform's ITradingService. So the
thing you ship is a venue plugin; the
Broker venues chapter covers that side. This chapter documents the
contract end to end so your provider behaves exactly like the built-ins.
Two seams: provider and service
There are two interfaces, and it matters which one you are looking at:
INTERFACE ITradingProvider
One trading-side data source. Owns accounts under a single ProviderKey, executes order
operations, and emits events. You implement this when integrating a venue.
INTERFACE ITradingService
The seam every consumer talks to: chart trader, strategies, add-ons, risk rules. Aggregates over N providers and presents one merged view. You consume this; the host provides it.
The service routes order operations to the right provider transparently: a caller identifies an order or
account by id and the service dispatches. Routing is keyed solely on
AccountId.ProviderKey. A request for an account whose provider isn't registered throws
InvalidOperationException: it is a configuration bug, and the caller lets it surface.
ITradingService.csusing SabrTrader.Pipeline.Venues.Trading;
// Aggregated snapshots across every registered provider.
// OpenOrders and GetOrder answer as of the last delivered event.
IReadOnlyList<Account> accounts = service.Accounts;
IReadOnlyList<Order> orders = service.OpenOrders;
IReadOnlyList<Position> positions = service.Positions;
// Lookup by id (cross-provider).
Account? acct = service.GetAccount(accountId); // null when no provider hosts it
Order? order = service.GetOrder(orderId); // null when not stated yet, long gone, or removed
// What may the UI offer for this account? None when its provider isn't registered.
TradingProviderCapabilities caps = service.GetCapabilities(accountId);
// One merged event stream, fanned out from all providers.
service.EventEmitted += OnTradingEvent;
OpenOrders and GetOrder answer as of the last delivered event, on the same clock as
Positions and EventEmitted: never ahead of what a subscriber has heard and never
behind it. An order placed a moment ago is listed once its first statement is delivered, though it can already
be cancelled or modified before that. An order the venue already finished stays listed until its terminal
statement is delivered. GetOrder returns the order's latest statement: working while it is listed
in OpenOrders, terminal for a bounded while after it ended (a rejection the service raised itself
included), and null otherwise (not stated yet, long gone, or its account or provider was removed).
The capability model
A provider declares what it supports with the TradingProviderCapabilities bitmask, and the
host greys out every action the provider leaves undeclared. An integration that only discovers accounts
and streams positions declares exactly those two flags; the trader then sees order buttons disabled on
those accounts instead of an error dialog after a click.
TradingProviderCapabilities.csusing SabrTrader.Pipeline.Venues.Trading;
[Flags]
public enum TradingProviderCapabilities
{
None = 0,
PlaceOrders = 1 << 0, // submit via PlaceAsync
CancelOrders = 1 << 1, // cancel via CancelAsync
ModifyOrders = 1 << 2, // modify via ModifyAsync (price / qty / TIF)
FlattenPositions = 1 << 3, // flatten via FlattenAsync
AccountDiscovery = 1 << 4, // streams AccountSnapshotEvent / AccountRemovedEvent
PositionStream = 1 << 5, // streams PositionUpdateEvent (live qty + PnL)
FillsStream = 1 << 6, // streams FillEvent per execution
Brackets = 1 << 7, // supports BracketSpec attached to orders
AssetBalances = 1 << 8, // streams BalanceSnapshotEvent (per-asset crypto wallets)
NativeOco = 1 << 9, // native one-cancels-other via PlaceOcoAsync
ShortSelling = 1 << 10, // may sell short / reverse a position
MultiLegOrders = 1 << 11, // atomic multi-leg (options) combos via PlaceMultiLegAsync
ExerciseOptions = 1 << 12, // ExerciseOptionAsync
Leverage = 1 << 13, // leveraged / derivative venue (DerivativeMetrics)
VenueRealizedPnl = 1 << 14, // Account.RealizedPnL is a real figure, not null
EnforcesOcoGroups = 1 << 15, // enforces an OcoGroupId on single PlaceAsync legs itself
ProtectionIsPositionLevel = 1 << 16, // protection lives on the position: one stop, one target
DurableOrderIds = 1 << 17, // broker order ids survive a reconnect and a restart
BrokerIdChangesOnModify = 1 << 18, // a modify gives the order a new broker id (cancel-replace)
SupportsAtm = 1 << 19, // keeps the promises the ATM decides on (promise kit)
RestatesOrdersAcrossSessions = 1 << 20, // replays working orders an earlier session placed
BookIsOpenTrades = 1 << 21, // everything the venue holds is an open trade, never a holding
Full = PlaceOrders | CancelOrders | ModifyOrders | FlattenPositions
| AccountDiscovery | PositionStream | FillsStream | Brackets | AssetBalances
| ShortSelling,
}
VenueRealizedPnl declares a nullability convention, not a feature. Set it only when
your venue reports TODAY's realized PnL on its account snapshots, so Account.RealizedPnL is a
real figure and 0m honestly means "flat on the day". Without the flag the field stays
null ("the venue does not report it"), and a consumer must never read that as
zero: fabricating 0m silently disables every PnL-based risk rule downstream.
The seven flags after it describe how the venue treats orders and positions rather than a feature to grey
out: whether the provider enforces OCO groups itself (EnforcesOcoGroups), whether protection
lives on the position (ProtectionIsPositionLevel), whether broker order ids survive a restart
(DurableOrderIds) or change on a modify (BrokerIdChangesOnModify), whether the
provider has passed the promises an ATM decides on (SupportsAtm), and whether it restates the
working orders an earlier session placed after a restart, a reconnect or a rebuild
(RestatesOrdersAcrossSessions). Without that last one, protection placed in an earlier session
may rest at the broker unseen, so the ATM refuses to adopt or re-protect such a trade unless the trader
confirms nothing rests. BookIsOpenTrades says everything the venue holds is an open trade
(futures, forex or crypto perpetuals), never a holding such as a stock or a spot coin: a position or order
row without a canonical instrument key may then be copied and flattened as a trade, and a flatten of the
account closes the whole of it. Without the flag such a row is a holding and is left alone.
Each is a promise the provider makes only when it is true, and all seven are opt-in
venue properties outside Full; Declaring
honestly says when.
ITradingService.GetCapabilities returns None for an account whose provider isn't
registered (e.g. disconnected mid-session), so the UI gates safely as if nothing is supported. Provider
keys themselves follow a convention, "<venue>" or
"<venue>:<qualifier>", documented under TradingProviderKeys;
use TradingProviderKeys.VenueOf(key) when you need the venue semantics rather than the exact
instance.
Connection lifecycle
The connection layer constructs the provider, registers it with the service and then starts it, so the
service hears its first account snapshot and its BooksSyncedEvent.
Starting and stopping a provider belongs to whoever constructed it. AddProvider throws if a provider with the same ProviderKey is already
registered (keys are unique per service), and when the provider is already running: the service learns a
provider's book only from what it states, so what a running provider already holds would never reach a
consumer. RemoveProvider detaches a provider's stream (it never stops the provider) and
withdraws, with one AccountRemovedEvent each, the accounts the provider stated and did not
withdraw itself, after the events it already queued. To take a provider down, the connection layer calls
ITradingService.BeginDetach first, then StopAsync, then RemoveProvider:
from BeginDetach on, the service's broker facts read the provider's statements as those of a
session that is ending. A stopping provider withdraws its accounts with AccountRemovedEvent
(never with made-up flats) and states nothing after that. Every connect, a reconnect included, raises its own ConnectionStateEvent
and, once the provider's book is published, its own BooksSyncedEvent. A provider that cannot
vouch for its whole book raises a ReplayCompletedEvent instead, and an account that comes up
after the books-synced gets its own AccountBookSyncedEvent. The service raises a
ProviderLifecycleEvent each time how far a provider's statements can be trusted changes
(see Broker facts).
graph TD;
A[Connection layer constructs provider]-->B[service.AddProvider];
B-->C[provider.StartAsync];
C-->D[connected, book published, BooksSyncedEvent];
D-->E[events flow into merged stream];
E-->F[service.BeginDetach];
F-->G[provider.StopAsync closes session];
G-->H[service.RemoveProvider detaches stream, withdraws leftover accounts];
EventEmitted after startup
receives only the events raised after it subscribed. On attach, read the snapshot
properties (Accounts, OpenOrders, Positions) to learn current state,
then follow the stream for changes.
Event-driven by design
Order operations are async and return quickly; the real outcome arrives later on the event
stream. PlaceAsync hands you an OrderId as soon
as the request is accepted locally; whether the order works, fills, or is rejected by the broker comes
through EventEmitted as an OrderUpdateEvent. This snapshot-plus-events shape is
what keeps the UI binding-friendly and reconciliation-safe across reconnects. The
Order & account updates page covers the full event model.
Namespaces & where things live
| Concern | Namespace | Key types |
|---|---|---|
| Core trading | SabrTrader.Pipeline.Venues.Trading |
ITradingProvider, ITradingService, Order, OrderRequest, Position, Account, TradingEvent
|
| Risk validation | SabrTrader.Pipeline.Venues.Trading.Risk |
IRiskValidator, RiskLimits, RiskRejection
|
| Commissions | SabrTrader.Pipeline.Venues.Trading.Commissions |
ICommissionModel, CommissionRate, RateCommissionModel, IVenueCommissionRateSource
|
| Broker facts | SabrTrader.Pipeline.Venues.Trading.Facts |
IBrokerFacts, BrokerFactsTable, ProviderFactsStatus
|
| Brackets | SabrTrader.Pipeline.Venues.Trading.Brackets |
BracketPreset
|
| Client-side OCO | SabrTrader.Pipeline.Venues.Trading.Oco |
ClientSideOcoCoordinator, IOcoLegGateway, IOcoIntentStore
|
| Automated trade management | SabrTrader.Pipeline.Venues.Trading.Atm |
AtmStrategy, AtmBracket, AtmStopStrategy, AtmStrategyAuthoring
|
A strategy uses the ATM model through Strategy.UseAtm; the
Advanced trade management page explains it, and the
Trading reference lists every ATM type.