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 brokers (Rithmic, Tradovate, CQG) 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, log tab, 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 (that's a configuration bug, not a runtime condition to swallow).

ITradingService.csusing SabrTrader.Pipeline.Venues.Trading;

// Aggregated snapshots across every registered provider.
IReadOnlyList<Account>  accounts  = service.Accounts;
IReadOnlyList<Order>    orders    = service.OpenOrders;
IReadOnlyList<Position> positions = service.Positions;

// Lookup by id (cross-provider). Null when nothing hosts it.
Account? acct  = service.GetAccount(accountId);
Order?   order = service.GetOrder(orderId);

// 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;

The capability model

A provider rarely supports everything at once — a phase-one integration might only discover accounts and stream positions, with order placement coming later. Rather than throwing NotSupportedException when the user clicks an unsupported action, a provider declares a fine-grained TradingProviderCapabilities bitmask and the host greys out actions it does not advertise. This matches the convention traders expect: the button is simply unavailable, not an error dialog.

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)

    Full = PlaceOrders | CancelOrders | ModifyOrders | FlattenPositions
         | AccountDiscovery | PositionStream | FillsStream | Brackets | AssetBalances
         | ShortSelling,
}

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 provider is constructed and started by the connection layer, then registered with the service. The service does not start or stop providers — that lifecycle belongs to whoever constructed it. AddProvider throws if a provider with the same ProviderKey is already registered (keys are unique per service); RemoveProvider detaches a provider's stream but does not stop the provider itself.

graph TD;
      A[Connection layer constructs provider]-->B[provider.StartAsync];
      B-->C[service.AddProvider];
      C-->D[events flow into merged stream];
      D-->E[service.RemoveProvider detaches stream];
      E-->F[provider.StopAsync closes session];
Snapshots vs. late subscribers. A subscriber attaching to EventEmitted after startup gets only future events — the stream is not replayed. 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, not as the method's return value. 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. The service does not re-marshal — UI subscribers must 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
Brackets SabrTrader.Pipeline.Venues.Trading.Brackets BracketPreset, IBracketPresetStore
Client-side OCO SabrTrader.Pipeline.Venues.Trading.Oco ClientSideOcoCoordinator, IOcoLegGateway, IOcoIntentStore
Simulation feed SabrTrader.Pipeline.Venues.Trading.Sim IPriceFeed, TickQuote

Automated trade management (ATM) — the IAtmOrderGateway port and AtmStrategy model that a strategy uses to manage protective orders — lives under SabrTrader.Pipeline.Venues.Trading.Atm but is documented in the Strategies → Advanced trade management page, not here.