Trading providers

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.

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.

Where your provider comes from. You do not register an 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];
Snapshots vs. late subscribers. A subscriber attaching to 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.

Threading. All provider and service methods are thread-safe, but events fire on whatever worker thread the provider chose, and the service delivers them on that thread. UI subscribers marshal to the dispatcher inside their own handler.

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.