Registration & capabilities

Brokerage gateways

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.

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);
}
One gateway per connection. 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 is permanent. The 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,
Phase your integration. Ship account read-only first by advertising only 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

  1. Build a class library that references TradeStrike.Pipeline.Brokerage.Contracts (for the gateway + DTOs), TradeStrike.Pipeline.Trading.Contracts (for AccountMode, the order enums, BracketSpec, AccountBalance), and TradeStrike.Pipeline.Contracts (for QuantityUnit, InstrumentCategory, ProviderCredentialField).
  2. Implement IBrokerageGateway + the three sub-gateways + an IBrokerageGatewayFactory, and expose a single static BrokerRegistration.
  3. 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.
  4. Honour the contract's error model: ConnectAsync and the stream-start methods throw on hard failure; order operations return a Rejected / Failed ack instead of throwing on a venue refusal.
Registration is a host-side wiring step. Like a trading provider, a brokerage gateway is not auto-discovered by dropping a DLL into a plugins folder — the broker catalog is assembled by the host's connection layer. You can build and unit-test your gateway against the contracts in complete isolation (fake the vendor SDK behind your sub-gateways); wiring a brand-new venue's 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.