Connections: trading providers
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.
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 newOrderRequeston an account. ThrowsArgumentExceptionon local validation failure andInvalidOperationExceptionfor an unknown account, but a broker reject (no buying power, market closed) does not throw — it surfaces as an order update with stateRejected. -
CancelAsync(orderId)/ModifyAsync(orderId, mod)— act on a working order. Both returnfalsewhen the order is unknown or already terminal, so you can race a cancel against a fill and treatfalseas "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.
OrderRequest, 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.