Connections: trading providers

Connections & data

Trading providers

The execution side of a connection: hosting accounts, placing/cancelling/modifying orders, flattening positions, and streaming every state change back out as events.

Data versus trading

Everything so far in this chapter has been about market data — bars, ticks, depth — modelled by IDataProvider. Order routing is a separate concern with its own contract, ITradingProvider (namespace TradeStrike.Pipeline.Trading). A real broker connection typically implements both: an IDataProvider for prices and an ITradingProvider for execution, sharing the same provider key so accounts and instruments line up. The built-in simulator, Rithmic and the crypto venues all follow this shape.

Discovery is different for trading. Unlike IDataProviderFactory, which the plugin loader auto-discovers and drops into a catalog, an ITradingProvider is not auto-registered by dropping a DLL today. Trading providers are brought online through the host's connection layer and aggregated by the platform's ITradingService. So you can implement ITradingProvider to model and test a broker integration against the contract, but wiring a brand-new trading venue into the running app is a host-side step, not a pure drop-in plugin like a data provider. This page documents the contract so you understand the trading model end to end.

The ITradingProvider contract

A trading provider owns a set of accounts under one ProviderKey, accepts order operations, and pushes out a stream of TradingEvents as state evolves. It is built around snapshots plus events: at any moment you can read the current accounts, open orders and positions, and between reads the EventEmitted stream tells you what changed.

contractpublic interface ITradingProvider : IDisposable
{
    string ProviderKey { get; }                       // matches the connection + AccountId.ProviderKey
    TradingProviderCapabilities Capabilities { get; }   // what the UI may offer (see below)
    bool IsRunning { get; }

    // Live snapshots — always the most recent committed state.
    IReadOnlyList<Account>  Accounts   { get; }
    IReadOnlyList<Order>    OpenOrders { get; }   // working / partially-filled only
    IReadOnlyList<Position> Positions  { get; }   // non-flat only

    // Push-stream of state changes (see "Order & account updates").
    event Action<TradingEvent>? EventEmitted;

    Task<bool>    StartAsync(CancellationToken ct = default);   // open session, load/subscribe accounts
    Task        StopAsync(CancellationToken ct = default);    // close cleanly

    Task<OrderId> PlaceAsync(OrderRequest request, AccountId account, CancellationToken ct = default);
    Task<bool>    CancelAsync(OrderId orderId, CancellationToken ct = default);
    Task<bool>    ModifyAsync(OrderId orderId, OrderModification mod, CancellationToken ct = default);
    Task<bool>    FlattenAsync(AccountId account, string? instrument = null, CancellationToken ct = default);
}

Order operations

The four operations are async and return quickly — the real outcome arrives later on the event stream, not as the method's return value. This is important: PlaceAsync hands you an OrderId as soon as the request is accepted locally, but whether the order works, fills or is rejected by the broker comes through EventEmitted as an OrderUpdateEvent.

  • PlaceAsync(request, account) — submit a new OrderRequest on an account. Throws ArgumentException on local validation failure and InvalidOperationException for an unknown account, but a broker reject (no buying power, market closed) does not throw — it surfaces as an order update with state Rejected.
  • CancelAsync(orderId) / ModifyAsync(orderId, mod) — act on a working order. Both return false when the order is unknown or already terminal, so you can race a cancel against a fill and treat false as "too late, already happened" without special error handling.
  • FlattenAsync(account, instrument?) — cancel every working order and close every open position on the account, optionally scoped to one instrument ("flatten my MNQ only"). The resulting cancels and market-close orders flow through the event stream.
Order, account and instrument typesOrderRequest, BracketSpec, OrderModification, Order, OrderState, Position and Account — are the same records the strategy API uses. They are documented in Strategies → Raw orders & brackets and the Trading reference.

Capabilities

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 (flags)PlaceOrders | CancelOrders | ModifyOrders | FlattenPositions
| AccountDiscovery     // streams AccountSnapshotEvent / AccountRemovedEvent
| PositionStream       // streams PositionUpdateEvent (live qty + PnL)
| FillsStream          // streams FillEvent per execution
| Brackets             // supports BracketSpec attached to orders
| AssetBalances        // streams BalanceSnapshotEvent (per-asset crypto wallets)
| Full                 // convenience: everything above

Snapshots & lifecycle

Accounts, OpenOrders and Positions always return the latest committed snapshot — reads are lock-free, and the provider swaps the underlying lists atomically inside its mutation section. OpenOrders contains only working or partially-filled orders (terminal ones drop out on the next snapshot), and Positions contains only non-flat positions (a position that goes flat emits one final update, then disappears). A late subscriber to EventEmitted gets only future events, so on attach you read the snapshot properties to learn current state, then follow the stream for changes.

Threading. All methods are thread-safe, and events fire on whatever worker thread the provider chooses — subscribers must marshal to the UI dispatcher inside their handler. The provider does not re-marshal for you.