Contract kit & capabilities
Contract kit & capabilities
Declaring capability across both layers, publishing the session ports, keeping saved profiles byte-compatible, and passing the two merge gates every trading venue is held to.
On this page
Two capability layers
A broker declares capability twice, at two different times, and the two are not redundant.
| Layer | Where | Read when | Meaning |
|---|---|---|---|
VenueCapabilities |
The manifest | Before any session exists | INTENT — what this venue can ever do. Drives dialog grouping and which host registrars schedule work. |
TradingProviderCapabilities |
The trading port | Per connected session | FACT — what this session can do right now. May be narrower (entitlement, account tier, partial connect); never wider. |
ProviderCapabilities |
The data port | Per connected session | FACT for the data plane, same rule. |
Narrowing at runtime is not a bug — it is the point. A broker whose Flex credentials are absent genuinely cannot serve trade history this session, and expresses that by not publishing the port. The host logs a wider-than-declared port as a venue bug at bring-up.
Declaring honestly
Flags are deliberately fine-grained so a partial implementation can declare exactly what it
supports. A read-only "show me my live positions" phase declares
AccountDiscovery | PositionStream and nothing else; the UI greys out the rest instead
of throwing at click time.
| Flag | Only when |
|---|---|
Brackets |
A BracketSpec can be attached to an entry order. |
NativeOco |
The venue cancels the surviving leg — so the link survives an app crash. Excluded from
Full: it is an opt-in venue property, never implied. |
ShortSelling |
The account can sell to open and reverse. A cash equity or spot-crypto account omits it. |
AssetBalances |
The account holds many assets rather than one cash figure. |
MultiLegOrders · ExerciseOptions
|
The venue has the atomic endpoint. Both excluded from Full. |
Leverage |
Leveraged derivatives (crypto perps). A COARSE UI gate only — actual limits are per-instrument metadata, never encoded here. |
VenueRealizedPnl |
The venue reports TODAY's realized PnL on its account snapshots. Without it,
Account.RealizedPnL must stay null. |
Synthesised capabilities
A capability your plugin implements by synthesis still counts — declare it. Modify via
cancel-replace is ModifyOrders. Flatten via cancel-all plus market-close is
FlattenPositions. The contract kit then pins that the declared capability actually
works, which is exactly the check that keeps a synthesis honest as the vendor API drifts.
OrderRevisionTracker — see
the components table.Session ports
Side channels are ordinary session ports, probed with AsPort<T>() and
registered by the host's port registrar while the session is connected. Declare the matching
manifest flag so the registrar looks for them.
| Port | Manifest flag | Notes |
|---|---|---|
IVenueTradeHistorySource |
TradeHistoryBackfill |
Supports(account) answers for accounts behind THIS login;
GetFillsAsync(account, fromUtc, toUtc, ct) reports the window's fills as a
VenueTradeHistoryPage. Read-only: the host owns the store write and the coverage
bookkeeping. Set IsComplete false (or return
VenueTradeHistoryPage.Partial) whenever you could not serve the whole window — a
window reported complete is never requested again. |
IVenueCashActivitySource |
CashActivity |
Posted financing / interest / dividends, for statement-model venues. |
IOptionChainSource |
OptionChains |
Underlying search, chain fetch, live quote subscription. See Options. |
IVenueCashActivitySource.cspublic sealed record VenueCashActivity(
string AccountId, // the VENUE's raw account id — the host recorder scopes it
CashActivityKind Kind,
decimal Amount, // NORMATIVE sign: positive = collected, negative = paid
string Currency,
DateTime TimestampUtc,
string VenueTransactionId); // the dedup identity
public interface IVenueCashActivitySource
{
Task<IReadOnlyList<VenueCashActivity>> GetCashActivityAsync(
DateTime sinceUtc, CancellationToken ct = default);
}
Settings & credential rotation
A broker's legacy profile JSON typically nests: a UsePaper boolean at the root, a
PlainValues object, a ProtectedSecretsBase64 object, and a
GatewaySecretsBase64 bag for vendor tokens. All of it rides the schema's dotted
StorageName mechanism, so the shape survives the move byte-for-byte.
Broker settings schemaSettingsSchema: new[]
{
new ProviderCredentialField("UsePaper", "Paper trading",
ProviderCredentialKind.Toggle, Required: false, DefaultValue: "false"),
new ProviderCredentialField("Environment", "Environment",
ProviderCredentialKind.Choice,
Choices: new[] { "Live", "Demo" },
StorageName: "PlainValues.Environment"),
new ProviderCredentialField("ClientId", "Client ID",
ProviderCredentialKind.Plain,
StorageName: "PlainValues.ClientId"),
new ProviderCredentialField("ClientSecret", "Client secret",
ProviderCredentialKind.Secret,
StorageName: "ProtectedSecretsBase64.ClientSecret"),
// Rotating OAuth tokens: written back through IVenueCredentialWriter and stored
// in the legacy encrypted bag, so the next start does not re-prompt for login.
new ProviderCredentialField("RefreshToken", "Refresh token",
ProviderCredentialKind.Secret, Required: false, ReadOnly: true,
StorageName: "GatewaySecretsBase64.refresh_token"),
}
Persisting a refresh// After every successful token refresh — not just the first.
context.Credentials.Persist(new Dictionary<string, string>
{
["AccessToken"] = tokens.Access,
["RefreshToken"] = tokens.Refresh,
});
The trading contract kit
TradingProviderContractKit (in SabrTrader.Pipeline.Venues.Tests) is the
merge gate for any direct ITradingProvider implementation. You supply a harness — a
connected provider over a scripted venue double — and the kit drives the incident-derived
behavioural contracts through it.
ITradingProviderContractHarness.cspublic interface ITradingProviderContractHarness : IAsyncDisposable
{
ITradingProvider Provider { get; } // connected, under test
AccountId Account { get; } // must appear in Provider.Accounts
OrderRequest CreateRestingOrderRequest(); // acked Working, never filled unprompted
Task FillCompletelyAsync(Order order); // script the venue to fill it
}
public sealed class MyBrokerContractTests : TradingProviderContractKit
{
protected override async Task<ITradingProviderContractHarness> CreateConnectedHarnessAsync()
=> await MyBrokerHarness.StartAsync();
}
| Check | The incident behind it |
|---|---|
| Cancel of an unknown order is false, never a throw | An emergency flatten sweep cancels ids it may no longer own; one throw aborts the sweep and leaves live orders on an account the trader believes is flat. |
| Cancel of a terminal order is false | The ATM races its cancel against a fill and treats false as "too late". |
| Cancel of a working order cancels exactly once and never flips to Filled | Terminal must be terminal — a re-opened order is an unmanaged position. |
CancelLegAsync of a known-terminal order is false AND still tracked terminal |
The OCO dual contract (2026-08-05): false means "retry", and TryGetOrder is the
coordinator's only short-circuit. False + untracked = an unbounded retry alarm. |
RealizedPnL is null when VenueRealizedPnl is not declared |
A fabricated 0m silently disables every PnL-based risk rule. |
Fills carry zero commission when ReportsCommission is false |
The live commission decorator enriches fills when it is false; a venue-reported figure under false double-charges. |
The seam conformance kit
Every venue, trading or not, also subclasses VenuePluginConformanceKit — loader
compatibility, manifest validity, verbatim legacy TypeIds, schema well-formedness, contracts-only
references, offline lifecycle safety. See
Instruments & discovery for the full
check list.
MyBrokerConformanceTests.cspublic sealed class MyBrokerVenuePluginConformanceTests : VenuePluginConformanceKit
{
protected override IVenuePlugin CreatePlugin() => new MyBrokerVenuePlugin();
protected override IReadOnlyList<string> LegacyTypeIds => new[] { "mybroker" };
}
Shipping the broker
- One DLL per broker, publishing the FULL transitive vendor closure from its
CopyLocalLockFileAssembliesbin — never a hand-written dependency list. - Native assets (
runtimes\) and content files ship with it. A vendor sidecar in the app root resolves through the plugin load context's last-chance hook. - Build with
-p:SkipPluginDeploy=truewhen an app instance may hold the DLL. - Run all three suites before merging: your own tests, the conformance kit, and the trading contract kit.
MaxConcurrentSessions above 1 and gets an independent session per
profile. Note that a mode qualifier alone collides for two same-mode logins — the host keys on the
profile, so your session must not assume a single global vendor client.