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 Market data connections chapter. Order routing is a separate concern with its own contract, ITradingProvider (namespace TradeStrike.Pipeline.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.

Discovery is different for trading. Unlike a data provider, an ITradingProvider is not auto-registered by dropping a DLL. Trading providers are constructed by the host's connection layer, started, and then handed to the platform's ITradingService via AddProvider. You can implement and test a broker integration against the contract in isolation, but wiring a brand-new venue into the running app is a host-side step. 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 TradeStrike.Pipeline.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 TradeStrike.Pipeline.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)

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

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 TradeStrike.Pipeline.Trading ITradingProvider, ITradingService, Order, OrderRequest, Position, Account, TradingEvent
Risk validation TradeStrike.Pipeline.Trading.Risk IRiskValidator, RiskLimits, RiskRejection
Commissions TradeStrike.Pipeline.Trading.Commissions ICommissionModel, CommissionRate, RateCommissionModel
Brackets TradeStrike.Pipeline.Trading.Brackets BracketPreset, IBracketPresetStore
Simulation feed TradeStrike.Pipeline.Trading.Sim IPriceFeed, TickQuote

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