Registration & capabilities
Registration & capabilities
One registration row plus a factory is all it takes to add a broker: the generic data, connection, and trading providers are driven entirely from the row, so no per-broker provider code is needed.
On this page
IBrokerageGatewayFactory
The factory creates a fresh IBrokerageGateway for one broker. It is a typed factory
— not a Func<IServiceProvider, …> — so the per-broker assembly states
its real dependencies (a logger, an HTTP client, a clock) in the factory's own constructor, with no
service-locator. The factory is held on a BrokerRegistration; the connection layer calls
Create once per connection to get a not-yet-connected gateway it then drives through
ConnectAsync.
IBrokerageGatewayFactory.csusing TradeStrike.Pipeline.Brokerage;
public interface IBrokerageGatewayFactory
{
// Create a new, not-yet-connected gateway instance.
IBrokerageGateway Create();
}
AcmeBrokerageGatewayFactory.csusing TradeStrike.Pipeline.Brokerage;
public sealed class AcmeBrokerageGatewayFactory : IBrokerageGatewayFactory
{
private readonly Action<string>? _log;
// Real dependencies are stated here — no service locator inside the gateway.
public AcmeBrokerageGatewayFactory(Action<string>? log = null) => _log = log;
public IBrokerageGateway Create() => new AcmeBrokerageGateway(_log);
}
Create returns a fresh instance each time. The connection
layer never reuses a disposed gateway — reconnecting builds a new one. Keep Create cheap
and side-effect-free; all the I/O belongs in ConnectAsync.
BrokerRegistration
BrokerRegistration is one row describing a supported broker — the broker analogue of the
crypto layer's exchange registration. Adding a broker is a registration row plus a gateway
implementation; the generic data / connection / trading providers read everything they need from this
record plus the GatewayFactory.
BrokerRegistration.csusing TradeStrike.Pipeline.Brokerage;
using TradeStrike.Pipeline.Providers; // ProviderCredentialField
public sealed record BrokerRegistration(
string TypeId, // stable lower-cased key + persisted-profile key, e.g. "alpaca"
string DisplayName, // label in the connection dialog, e.g. "Alpaca"
BrokerCapabilities Capabilities, // what the gateway supports → provider bitmasks + UI gating
bool SupportsPaper, // true → adds the "Use paper account" toggle
bool HasOwnMarketData, // false → charts route to the asset-class data fallback
bool ReportsCommission, // true → the trading layer must NOT also apply its own model
IReadOnlyList<ProviderCredentialField> Credentials, // the fields the gateway needs (rendered by Desktop)
IBrokerageGatewayFactory GatewayFactory); // creates a fresh gateway (typed — no service locator)
TypeId (lower-cased) is the provider key and the
persisted-profile key. Renaming it breaks every saved connection profile for that broker. Pick it once.
The registration is deliberately venue-neutral (Pipeline layer): there is no AssetClass here
— that is a Desktop type. The asset-class list is paired with this registration on the Desktop
connection provider. The three booleans are per-broker facts that the layer cannot guess, so they live on
the row:
| Flag | Meaning |
|---|---|
SupportsPaper |
The broker offers a paper/demo endpoint — adds the "Use paper account" toggle, and ConnectAsync receives AccountMode.Paper. |
HasOwnMarketData |
The gateway serves its own market data (its Data sub-gateway is non-null). When false, charts route to the asset-class data fallback (e.g. dxFeed for equities). |
ReportsCommission |
The broker reports real commission/fees on its fills, so the trading layer must not also apply its own commission model (avoids double-charging). False for a commission-free broker — the local commission settings then apply. |
Declaring credentials
The broker self-describes the credentials it needs using the SDK's existing
ProviderCredentialField (namespace TradeStrike.Pipeline.Providers) — the same
type a data-provider factory uses — rather than a new field type. The Desktop connection dialog
renders one input per field; on connect the host hands those values to the gateway as a
BrokerCredentials bag keyed by ProviderCredentialField.Key.
ProviderCredentialField.csusing TradeStrike.Pipeline.Providers;
public sealed record ProviderCredentialField(
string Key, // the key your gateway reads via credentials.Require(Key)
string DisplayName, // the field label in the dialog
ProviderCredentialKind Kind, // Plain / Secret / Choice / Toggle / Url
string? Placeholder = null,
bool Required = true);
public enum ProviderCredentialKind { Plain, Secret, Choice, Toggle, Url }
Use ProviderCredentialKind.Secret for anything sensitive (API secrets, passwords) so the
dialog masks the input and the value is stored encrypted (DPAPI). The Key you declare here is
exactly the key your gateway reads with credentials.Require("ApiKey") — keep them in
sync.
Capability negotiation
BrokerCapabilities appears in two places, and they must agree: on the registration row (so the
host knows up front what to offer) and on the live IBrokerageGateway.Capabilities property.
The generic providers translate them into the platform's own ProviderCapabilities /
TradingProviderCapabilities bitmasks, so an unadvertised feature is simply greyed out —
never a NotSupportedException at click time.
Declare the capability bitmask once as a constant and reference it from both the gateway and the registration, so they can never drift:
Capabilities wired in two placesusing TradeStrike.Pipeline.Brokerage;
public sealed class AcmeBrokerageGateway : IBrokerageGateway
{
public const BrokerCapabilities AcmeCapabilities =
BrokerCapabilities.HistoricalBars | BrokerCapabilities.LiveTrades | BrokerCapabilities.Instruments |
BrokerCapabilities.PlaceOrders | BrokerCapabilities.CancelOrders | BrokerCapabilities.ModifyOrders |
BrokerCapabilities.FlattenPositions | BrokerCapabilities.Positions;
public BrokerCapabilities Capabilities => AcmeCapabilities; // live gateway
// …
}
// …and the registration references the SAME constant:
// Capabilities: AcmeBrokerageGateway.AcmeCapabilities,
Positions /
Balances and returning null Trading / Data. Add
PlaceOrders / CancelOrders later by flipping flags and filling in the sub-gateway
— no host change. Advertise ModifyOrders only when your
Trading.ModifyOrderAsync truly amends natively; otherwise leave it off and the provider does
cancel-replace for you (the user still sees a working "modify").
A complete registration
This is the single static row that declares a broker to TradeStrike — the shape the built-in Alpaca
adapter uses. The generic connection / data / trading providers are driven entirely from this
BrokerRegistration plus the factory, so adding the broker to the app is just adding this row to
the broker catalog.
AcmeBroker.csusing TradeStrike.Pipeline.Brokerage;
using TradeStrike.Pipeline.Providers;
public static class AcmeBroker
{
// Stable provider key / persisted-profile key. Renaming breaks saved Acme profiles.
public const string TypeId = "acme";
public static BrokerRegistration Registration { get; } = new(
TypeId: TypeId,
DisplayName: "Acme Markets",
Capabilities: AcmeBrokerageGateway.AcmeCapabilities,
SupportsPaper: true, // Acme has a first-class paper endpoint
HasOwnMarketData: true, // serves its own bars + trade stream
ReportsCommission: false, // commission-free → local commission settings apply
Credentials: new[]
{
new ProviderCredentialField("ApiKey", "API Key ID", ProviderCredentialKind.Secret),
new ProviderCredentialField("ApiSecret", "API Secret Key", ProviderCredentialKind.Secret),
},
GatewayFactory: new AcmeBrokerageGatewayFactory());
}
At runtime the flow is: the host renders the two credential fields, the user picks Live or Paper, the
connection layer calls Registration.GatewayFactory.Create(), builds a
BrokerCredentials bag from the saved profile, and calls
gateway.ConnectAsync(credentials, mode). From there the generic providers pull bars, route
orders, and stream account state through your three sub-gateways.
graph TD;
U[User picks broker + Live/Paper]-->H[host reads BrokerRegistration];
H-->F[Registration.GatewayFactory.Create];
F-->G[fresh IBrokerageGateway];
H-->B[build BrokerCredentials from saved profile];
B-->C[gateway.ConnectAsync credentials + mode];
C-->P[generic data / trading / account providers go live];
Deployment
- Build a class library that references
TradeStrike.Pipeline.Brokerage.Contracts(for the gateway + DTOs),TradeStrike.Pipeline.Trading.Contracts(forAccountMode, the order enums,BracketSpec,AccountBalance), andTradeStrike.Pipeline.Contracts(forQuantityUnit,InstrumentCategory,ProviderCredentialField). - Implement
IBrokerageGateway+ the three sub-gateways + anIBrokerageGatewayFactory, and expose a single staticBrokerRegistration. - Keep every vendor-SDK type inside the assembly — only venue-neutral DTOs cross the port.
Map canonical symbols (e.g.
"EUR/USD") to the venue's native notation in your gateway, not upstream. - Honour the contract's error model:
ConnectAsyncand the stream-start methods throw on hard failure; order operations return aRejected/Failedack instead of throwing on a venue refusal.
BrokerRegistration into the running catalog is a host integration step. This chapter documents
the contract end to end so your gateway behaves exactly like the built-ins.