The order-routing surface: the trading provider and service, the order/position/account
model, all order enums, trading events, risk validation, commission models, bracket presets, and the ATM
advanced-trade-management types.
Provider & service: SabrTrader.Pipeline.Venues.Trading
An ITradingProvider is one connected brokerage; the host aggregates providers behind an
ITradingService and fans out a single TradingEvent stream. Both expose the same
place / cancel / modify / flatten verbs.
| Type |
Members |
ITradingProvider : IDisposable
|
Raises EventEmitted in book order, never nested inside another delivery; withdraws every account it stops hosting with AccountRemovedEvent, never with made-up flats; StopAsync and Dispose never wait for delivery. See Event ordering. ProviderKey, Capabilities, bool ReportsCommission (default false: true when every fill carries the venue's own fee), bool? RealizedPnLIncludesCommissions (default null: whether Account.RealizedPnL is net of fees, see Venue-reported fees), IsRunning, Exception? LastStartError, Accounts, OpenOrders, Positions; event Action<TradingEvent>? EventEmitted; Task<bool> StartAsync(ct), Task StopAsync(ct), Task<OrderId> PlaceAsync(OrderRequest, AccountId, ct), Task<bool> CancelAsync(OrderId, ct), Task<bool> ModifyAsync(OrderId, OrderModification, ct), Task<bool> FlattenAsync(AccountId, string? instrument = null, ct), Task<bool> ExerciseOptionAsync(AccountId, string instrument, decimal quantity, ct), Task<MultiLegPlacement> PlaceMultiLegAsync(MultiLegOrderRequest, AccountId, ct), Task<OcoPlacement> PlaceOcoAsync(OcoRequest, AccountId, ct), Task<MultiLegPrecheckResult> PrecheckMultiLegAsync(MultiLegOrderRequest, AccountId, ct), IReadOnlyList<AccountBalance> GetBalances(AccountId). |
ITradingService : IDisposable
|
Accounts, OpenOrders (every order stated working as of the last delivered event: on the same clock as Positions and EventEmitted, so an order placed a moment ago is listed once its first statement is delivered, though it can be cancelled or modified before that, and an order the venue already finished is listed until its terminal statement is delivered), Positions, Providers; Account? GetAccount(AccountId), Order? GetOrder(OrderId) (the order's latest statement as of the last delivered event: working while listed in OpenOrders, terminal for a bounded while after it ended, a rejection the service raised itself included, otherwise null: not stated yet, long gone, or its account or provider was removed), TradingProviderCapabilities GetCapabilities(AccountId), IReadOnlyList<TimeInForceChoice> GetTimeInForceChoices(AccountId, VenueAssetClass) (the time-in-force choices the account's venue takes for an instrument of that asset class, the venue's default first: what an order screen offers; TimeInForceOffers.Standard when the venue declares no list or the account's provider is not registered; the same list instance comes back while the answer is the same, so a screen compares by reference to see whether to rebuild), IReadOnlyList<AccountBalance> GetBalances(AccountId); event Action<TradingEvent>? EventEmitted; void AddProvider(ITradingProvider) (throws InvalidOperationException when the key is already registered or the provider is already running: attach before StartAsync), void BeginDetach(string providerKey) (the provider is about to be stopped: whoever stops providers calls it before StopAsync, and from then on the broker facts read it Detaching; a decorator forwards it to the service it wraps), bool RemoveProvider(string providerKey) (after StopAsync; withdraws with one AccountRemovedEvent each the accounts the provider stated and never withdrew, after its queued events); same PlaceAsync / CancelAsync / ModifyAsync / FlattenAsync verbs (service also has FlattenAsync(account, instrument, instrumentKey, instrumentVenue, ct)) plus ExerciseOptionAsync, PlaceMultiLegAsync, PlaceOcoAsync, PrecheckMultiLegAsync. Entry pairs: Task<OcoLinkOutcome> LinkEntryPairAsync(OrderId first, OrderId second, ct) (links two working orders on one account and instrument one-cancels-other: from then on the first fill of either, a partial one included, cancels the other; the link is kept by whoever keeps the venue's groups, this service or a provider that keeps its own, and survives a restart where that owner stores its groups; it never throws for a runtime condition: a refusal comes back with its OcoLinkRefusal and both orders keep working as they were; the interface default refuses with VenueCannotLink), bool IsInEntryPair(OrderId order) (true while the order works as one of an entry pair, linked by LinkEntryPairAsync or placed in an EntryPair group: what a view marks as OCO; the default answers false), event Action? EntryPairsChanged (raised when the answer of IsInEntryPair may have changed without a statement of the orders concerned: two working orders were linked, a reconnect restored its links, or a pair lost one order and its survivor works alone; raised on the thread that made the change, never under a lock, so a view posts to its own thread and reads again). See Client-side OCO. |
TradingProviderCapabilities |
[Flags] enum: None, PlaceOrders, CancelOrders, ModifyOrders, FlattenPositions, AccountDiscovery, PositionStream, FillsStream, Brackets, AssetBalances, NativeOco, ShortSelling, MultiLegOrders, ExerciseOptions, Leverage, VenueRealizedPnl, EnforcesOcoGroups, ProtectionIsPositionLevel, DurableOrderIds, BrokerIdChangesOnModify, SupportsAtm, RestatesOrdersAcrossSessions, BookIsOpenTrades, Full. Full is PlaceOrders through AssetBalances plus ShortSelling; every other flag is an opt-in venue property. BookIsOpenTrades says everything the venue holds is an open trade (futures, forex or crypto perpetuals), never a holding (a stock, a fund, an option, a spot coin): a position or order row without a canonical instrument key may then be copied and flattened as a trade, and a flatten of the account closes the whole of it. Without the flag an unkeyed row is a holding and is left alone. Declaring honestly says when each may be declared. |
ITradingProviderDecorator · TradingProviderCapabilityProbes
|
ITradingProvider Inner on a provider that wraps another; and the extension T? AsCapability<T>(this ITradingProvider?), which finds an optional facet (IPositionRefresh, IPositionPropagationDelay, IVenueCommissionRateSource, IOpenOrdersSnapshot, IOpenPositionsSnapshot, IOrderStatusRequest, IPositionResync, IOcoCoordinatorHost, IOcoGroupLinking) on the provider or any provider it wraps. |
FlattenOutcome · FlattenResult
|
readonly record struct (FlattenResult Result, string Reason = "", string? Instrument = null) with IsClosed, IsStillOpen and factories Closed(instrument?), Refused(reason, instrument?), NotSent(reason, instrument?), Unknown(reason, instrument?); enum Closed (nothing in scope remains and no order can still fill into it: the only value a caller may act on, for example a reverse entering the other side), Refused (the venue refused; still open), NotSent (nothing was sent and nothing changed; safe to retry), Unknown (the adapter cannot tell; tell the trader and change nothing). What an adapter knows a flatten achieved. FlattenAsync returns bool on both interfaces, and a refusal travels as FlattenRefusedEvent. |
ProviderEventOutbox |
sealed class: a provider's one ordered way out. ProviderEventOutbox(Action<TradingEvent> deliver, Action<Exception> onDeliveryFault); long Stage(TradingEvent evt) (queue behind everything staged before it and return its sequence; runs no consumer code, so call it under the lock that wrote the state), void Flush() (deliver everything staged, in order, one at a time; returns at once when another thread is delivering, which delivers these too; call it holding no provider lock), void FlushThrough(long sequence) (a command's flush: returns once every event up to sequence is delivered; returns at once on the delivering thread; never from a venue thread, never in Stop or Dispose), long LastStaged (the sequence of the last event staged), int CommandsWaiting (how many commands are in FlushThrough waiting for another thread to deliver their events, so a provider's tests can tell a command is held there without timing it), void FlushCommand(long sequence, bool waitForDelivery) (the flush of a command that two kinds of caller share: with true it is FlushThrough(sequence), with false it is Flush(), for a caller that must never wait on a consumer, such as an OCO leg command issued on a venue thread), void ObserveOrders(Action<Order> observer) (one listener that hears every order statement staged after the call, apart from the consumers: the provider's one-cancels-other coordinator, which must cancel a filled leg's sibling without waiting for a consumer; set it once, before the provider states an order the observer must hear; it runs on the thread that flushes, right after its flush, holding no provider lock, in staged order and one thread at a time; it may call back into the provider, and what it throws goes to onDeliveryFault). onDeliveryFault receives what deliver threw and delivery continues; it must not throw. Events staged during a delivery are delivered after it, never nested. See Using ProviderEventOutbox. |
TradingProviderKeys |
static: const char VenueQualifierSeparator, string VenueOf(string providerKey), string Compose(string venue, string qualifier), bool BelongsToVenue(string providerKey, string venue). |
IProtectionVerifier |
bool IsProtected(AccountId account, string instrument): true when the scope is flat, or its whole position is covered by working protective stop quantity on the exit side, as the broker states it. The feed-recovery lifecycle asks it before resuming a paused strategy. Read-only. |
Sequencing work on ITradingService
| Member |
Meaning |
bool Post(Action work) |
Run work on the service's own sequencing loop, in order with the trading events it is processing. False when the service cannot accept it. |
Checking the book against the venue
Optional ITradingProvider facets for a venue that can be asked directly what it holds. A
provider implements the ones its venue supports; the trading service probes each with
AsCapability<T>() and calls it. A provider without a facet is not asked, and nothing is
concluded for it.
| Type |
Members |
IOpenOrdersSnapshot |
Task<IReadOnlyList<VenueOpenOrder>> ReadOpenOrdersAsync(AccountId account, ct): every order resting at the venue on the account, read at the moment of the call; an empty list when the venue lists none. Throws when the venue could not be asked (a failed or cancelled request, an answer without the list): an unanswered read is never "no orders". TimeSpan ListLag (default 5 seconds): how long the venue's list can lag behind an order it just took or changed. For that long after an order was last stated, the list not showing it, or showing its old terms, says nothing. The trading service compares the answer with its own working orders. |
VenueOpenOrder |
readonly record struct (string VenueOrderId, OrderId? PlatformId, string? Instrument = null, OrderSide? Side = null, decimal? RemainingQuantity = null, decimal? LimitPrice = null, decimal? StopPrice = null): one order resting at a venue, as the venue itself lists it. PlatformId is the platform id the provider knows the order under, null for an order it cannot link to one (placed elsewhere, or before this session). The terms are stated only where the venue's list states them: null on a term means "the list did not say", never zero or none. |
IOrderStatusRequest |
Task<bool> AskOrderStatusAsync(OrderId orderId, ct): asks the venue what became of one order the provider holds. The trading service asks it for a working order the venue's open-orders list no longer carries, before it concludes anything from that. Three outcomes, never mixed. True: the venue knows the order, and when its answer differs from what the provider held (filled, cancelled, expired) the provider states it on EventEmitted as it states any other, fills included. False: the venue answered that it does not know the order, and nothing is stated. Throws: the venue could not be asked, or the provider holds no order under the id. An unanswered request is never "not known". An order the venue answers "not known" twice, a check apart, is ended by the service as Cancelled with OrderEndCause.VenueNoLongerKnowsIt. |
IOpenPositionsSnapshot |
Task<IReadOnlyList<Position>> ReadOpenPositionsAsync(AccountId account, ct): every position open at the venue on the account, read from the venue at the moment of the call and not from what the provider holds: one row per instrument, netted and signed as the provider states its positions, an empty list when the venue holds none. The read states and changes nothing. Throws when the venue could not be asked or did not answer the whole account: an unanswered read is never "no positions". The trading service compares the answer with the positions it holds. It is for a venue whose positions arrive on a stream; a venue that re-reads its positions itself every few seconds gains nothing from it. |
IPositionResync |
Task ResyncPositionsAsync(AccountId account, ct): reads the account's positions from the venue again and states every difference with what the provider holds on EventEmitted, the way it does after a reconnect: a position the venue no longer holds is stated flat, a changed one, or one the provider did not hold, with its size. Only a complete read states anything, and a position the venue's stream changed while the read was under way keeps the stream's word. Throws when the venue could not be asked or did not answer the whole account. The trading service asks for it when its own positions and the venue's read (IOpenPositionsSnapshot) keep differing. |
Orders & enums
Build an OrderRequest (optionally with a BracketSpec) and submit it; the resulting
Order carries its state and fills. OrderRequest.Validate() throws on malformed
requests; call it before placing.
| Type |
Members |
OrderRequest |
record (string Instrument, OrderSide Side, OrderType Type, decimal Quantity, decimal? LimitPrice = null, decimal? StopPrice = null, TimeInForce TimeInForce = Day, BracketSpec? Bracket = null, string? OcoGroupId = null, string? ClientTag = null, ...); init InstrumentKey, InstrumentVenue, OriginId, NativeInstrumentId, NativeInstrumentType, PositionEffect? PositionEffect, bool? PostOnly, bool? ReduceOnly, OrderSession Session (the trading sessions the order may fill in; Regular unless the placer sets it, and kept by a modification), OcoGroupKind OcoGroupKind (what the members of OcoGroupId are to each other); bool IsGroupedExit (true when the order carries an OcoGroupId and the group is not an EntryPair, whose members open a position: what a venue reads to treat a grouped order as protection, so a perp venue sends it reduce-only, and what a notifier reads to call its fill a stop or a target); void Validate(). |
BracketSpec |
record (decimal? StopLossPrice, decimal? TakeProfitPrice); void Validate(OrderSide entrySide). |
OrderModification |
record (decimal? NewQuantity = null, decimal? NewLimitPrice = null, decimal? NewStopPrice = null, TimeInForce? NewTimeInForce = null); bool TryApplyTo(OrderRequest current, out OrderRequest merged, out string? invalid): the request a live order carries after this modification, which is current with every field the modification names replaced, checked with OrderRequest.Validate before anything is sent to a venue. False when the merged request is not a valid order, with the reason in invalid. A provider that modifies by cancel-and-replace asks it first and cancels nothing when it answers false, so a modify that cannot be expressed never destroys a live protective order. |
Order |
record (OrderId Id, AccountId AccountId, OrderRequest Request, OrderRequest OriginalRequest, OrderState State, decimal FilledQuantity, decimal AverageFillPrice, string? BrokerOrderId, string? RejectReason, DateTime CreatedUtc, DateTime UpdatedUtc); IsTerminal, RemainingQuantity, bool RestsAtVenue (true while the venue holds the order on its book: Working, PartiallyFilled, PendingCancel or PendingModify; an order still on its way, Pending, or of unknown state, Stale, does not count); init OrderRejectOrigin? RejectOrigin (who refused the order: null unless the order is rejected, and null on a rejected order reads as OrderRejectOrigin.Venue), OrderEndCause? EndCause (why the order ended, where the platform knows it: null while it is open and when the cause was not stated), bool PlacedOutside (true when this session of the platform did not place the order: it was placed in another application, or by the platform before a restart, and is stated because the venue holds it; the trader sees it and can cancel or change it, and nothing automated treats it as its own; false for every order placed through the trading service); static Pending(id, accountId, request). |
Fill |
record (OrderId OrderId, decimal Quantity, decimal Price, decimal Commission, DateTime TimestampUtc, string? FeeCurrency = null); init string? ExecutionId (the venue's own fill id, when it has one), bool CommissionIsAuthoritative (true when Commission is the venue's own fee for this execution, a genuine 0 on a commission-free venue included; false means the platform may price the fill from the trader's commission settings). See Venue-reported fees. |
OrderId |
readonly record struct: Guid Value; static New(), bool TryParse(string?, out OrderId); ToString() is the 32-digit "N" form. |
OrderSide |
enum: Buy, Sell. |
OrderType |
enum: Market, Limit, Stop, StopLimit. |
TimeInForce |
enum: Day, Gtc, Ioc, Fok. How long an order lives. |
OrderSession |
enum: Regular (the venue's regular session only; the default), PreMarket (the session before the regular one only), PostMarket (the session after the regular one only), Extended (the pre-market, the regular session and the post-market as one order). The sessions an order may fill in, carried on OrderRequest.Session. |
TimeInForceChoice |
readonly record struct (TimeInForce TimeInForce, OrderSession Session = Regular). One entry of a venue's time-in-force list: a lifetime and the sessions it applies to. A screen shows it as one choice; an order carries the two halves as OrderRequest.TimeInForce and OrderRequest.Session. |
ITimeInForceOffer |
IReadOnlyList<TimeInForceChoice> TimeInForceChoices(VenueAssetClass assetClass): optional provider facet, probed with AsCapability<ITimeInForceOffer>(). The choices the venue takes for an instrument of that asset class, the venue's default first. VenueAssetClass.Unknown asks for the choices that hold whatever is traded, and those never name a session. The provider returns a list it holds for its lifetime: the same instance for the same answer, never empty. An order screen offers exactly this list, and the trading service sends an order that names a session only when the list holds that session with the order's time in force. |
TimeInForceOffers |
static: IReadOnlyList<TimeInForceChoice> Standard (Day, Gtc, Ioc, Fok for the regular session: the offer of a provider that declares none), IReadOnlyList<TimeInForceChoice> Of(ITradingProvider? provider, VenueAssetClass assetClass) (the provider's own list, found through any decorator, else Standard), bool Holds(IReadOnlyList<TimeInForceChoice> choices, TimeInForceChoice choice). |
OrderState |
enum: Pending, Working, PartiallyFilled, Filled, Cancelled, Rejected, Expired, Stale, PendingCancel (a cancel of the order was sent and the venue has not answered it: the order still rests and can still fill; it ends as Cancelled, or as Filled when a fill beat the cancel, and goes back to the state it had when the venue refuses the cancel), PendingModify (a change of price, quantity or time in force was sent and the venue has not answered it: the order still rests on its earlier terms and can still fill, and goes back to the state it had once the venue states the change or refuses it). PendingCancel and PendingModify are set by the trading service, never by a venue. |
OrderRejectOrigin |
enum, read from Order.RejectOrigin: OrderRejectOrigin.Venue (the venue refused the order, or does not take it by its own declaration: a session it does not offer, see ITimeInForceOffer), OrderRejectOrigin.Service (the trading service's own veto: a risk rule, a duplicate submission, the venue's execution session down; the path that also raises RiskRejectionEvent; nothing was sent), NotSent (nothing was sent: the provider refused before sending, or no provider is registered for the account). A consumer that retries or re-places reads this instead of the reason text: an order nothing was sent for can be asked again as soon as the cause clears, while an order the venue refused will be refused again. |
OrderEndCause |
enum, read from Order.EndCause: OwnModifyFailed (the order was lost by a change of its own: on a venue that changes an order by cancelling it and placing a replacement, the cancel took effect and nothing took the order's place), VenueNoLongerKnowsIt (the platform ended the order itself, without the venue's word: the venue's list of open orders no longer carried it and the venue answered twice, a check apart, that it does not know the order; the order is stated Cancelled, and what it protected may be unprotected). A consumer that has to act on an order's end reads this instead of the reason text. Null on an ended order means the venue ended it (a fill, a requested cancel, an expiry, a rejection) and said no more. |
PositionEffect |
enum: Open, Close. |
OcoGroupKind |
enum: Alternatives (the default: only one member is meant to trade, such as an entry pair, so a rejected member cancels its siblings rather than leave the group half-armed), ProtectiveExits (one position's stop and target: a fill of one still takes the others down, but a REJECTED member leaves its siblings working, because cancelling the stop when the target was refused leaves the position naked; the owner re-places the refused leg), EntryPair (two entries of which only one may open the position, such as a buy stop above a range and a sell stop below it: the first fill of any size, a partial one included, takes the others down, because another entry left working at a reduced size would trade against the position; a rejected member breaks the group as it does for Alternatives). The ATM marks its protective legs ProtectiveExits. Meaningless without an OcoGroupId. |
Multi-leg & OCO orders
Two placement families beyond the single OrderRequest: an atomic net-priced multi-leg
(options) order and an one-cancels-other pair. Gate each on the matching capability flag
(MultiLegOrders, NativeOco) and call the corresponding
PlaceMultiLegAsync / PlaceOcoAsync verb.
| Type |
Members |
MultiLegOrderRequest |
record (IReadOnlyList<OptionLeg> Legs, decimal NetLimitPrice, decimal Quantity, string? ClientTag = null); const int MinLegs = 2, const int MaxLegs = 4; init NetPriceDirection? Direction; OrderRequest ToRiskOrderRequest(OptionLeg leg), void Validate(). |
OptionLeg |
record (string Instrument, string NativeInstrumentId, string NativeInstrumentType, OrderSide Side, int Ratio, PositionEffect PositionEffect); init decimal? ReferencePrice. |
MultiLegPlacement |
record (bool Success, IReadOnlyList<OrderId> OrderIds, string? GroupId, string? RejectReason); static Ok(orderIds, groupId), Failed(reason). |
MultiLegPrecheckResult |
record (bool Ok, decimal? EstimatedCashRequired, string? Currency, string? Reason); static Unsupported(reason). |
OcoRequest |
record (string Instrument, IReadOnlyList<OcoLeg> Legs, string? InstrumentKey = null, string? InstrumentVenue = null, string? OriginId = null); OrderRequest ToOrderRequest(OcoLeg leg), void Validate(). |
OcoLeg |
record (OrderSide Side, OrderType Type, decimal Quantity, decimal? LimitPrice = null, decimal? StopPrice = null, TimeInForce TimeInForce = Day, string? ClientTag = null). |
OcoPlacement |
record (bool Success, IReadOnlyList<OrderId> LegOrderIds, string? FailureReason); init bool NothingRests; static Ok(legOrderIds), Failed(reason), Vetoed(reason). On success LegOrderIds lines up one-to-one with the request's legs. A failure is not proof that nothing rests: a group can fail after a leg went out. Only Vetoed (refused before any leg was sent: unsupported, not connected, a pre-trade veto) sets NothingRests; treat any other failure as of unknown fate before placing the group again. |
NetPriceDirection |
enum: Debit, Credit. |
Accounts & positions
| Type |
Members |
Account |
record (AccountId Id, string DisplayName, string Currency, decimal CashValue, decimal? RealizedPnL, decimal UnrealizedPnL, decimal MarginUsed, decimal BuyingPower, DateTime UpdatedUtc); init IsSnapshot, AccountMetrics? Metrics, PrefersDisplayNameInSelector, UnrealizedExcludedCount; computed Mode, Equity, SelectorLabel, IsUnrealizedPartial; static NewSim(...). |
AccountId |
readonly record struct: string ProviderKey, string ProviderAccountId, AccountMode Mode; static None; IsEmpty, string VenueKey (the provider key without its qualifier), AccountId ToVenueScoped() (the same account keyed by venue, for configuration that outlives a connection), bool SameVenueAccount(AccountId), ToString() ("providerKey/accountId/mode"), bool TryParse(string?, out AccountId). See AccountId & mode. |
AccountMode |
enum: Live, Paper, Sim. |
AccountBalance |
record: string Asset, decimal Total, decimal Available, decimal Reserved; static Create(asset, total, available). |
AccountMetrics |
record of nullable broker metrics: TotalCashBalance, GrossRealizedPnL, TotalCommissions, MaintenanceMargin, ExcessBuyMargin/ExcessSellMargin, UsedBuyingPower, DailyLossLimit, DrawdownPercentUsed, TrailingDrawdownFloor, NetQuantity, OpenPositions, FilledBuys/FilledSells, WorkingBuys/WorkingSells, … |
Position |
record (AccountId AccountId, string Instrument, decimal Quantity, decimal AverageEntryPrice, decimal LastPrice, decimal UnrealizedPnL, decimal RealizedPnLSession, DateTime OpenedUtc, DateTime UpdatedUtc, decimal Multiplier = 1m); IsLong/IsShort/IsFlat; init IsSnapshot, PositionMetrics? Metrics, string? PnlCurrency, string? InstrumentKey / string? InstrumentVenue (provider-neutral identity, stamped by the platform at book admission; a venue adapter leaves both null); ApplyFill(side, qty, price, time), MarkToMarket(price, time); static Empty(...). |
PositionMetrics |
record of nullable per-position metrics: decimal? Financing, DerivativeMetrics? Derivative; option fields OptionIsCall, OptionStrike, OptionExpiry, OptionUnderlyingSymbol, OptionCanExerciseEarly. |
DerivativeMetrics |
record of nullable derivative metrics: MarkPrice, LiquidationPrice, Leverage, MarginMode? MarginMode, PositionMarginUsed, FundingRate, NextFundingTime, UnrealizedFunding, PaidFunding. |
MarginMode |
enum: Cross, Isolated. |
Order provenance
| Type |
Members |
OrderOrigins |
static: const ChartPrefix = "chart:", StrategyPrefix = "strategy:", CopierPrefix = "copier:"; Chart(chartId), Strategy(strategyName), Copier(groupId); IsChart / IsStrategy / IsCopier(string?); OrderProvenance Parse(string?). The ONE place origin ids are formed and read: a consumer deciding whether a resting order is someone else's to manage must never guess at a prefix. |
OrderProvenance · ProvenanceKind
|
record (ProvenanceKind Kind, string Name) with static Unknown and From(string? originId); enum Unknown, ChartTrader, Strategy, Copier. Name carries the copier group or strategy name; empty for a Chart Trader instance. |
Position scopes
| Type |
Members |
PositionScopeKey |
readonly struct (AccountId account, string instrument) with value equality and == / !=. |
IPositionInstrumentIdentity |
bool TryResolve(string wireInstrument, string providerKey, out string instrumentKey, out string instrumentVenue): host-owned resolution of a venue's wire symbol to Position.InstrumentKey / InstrumentVenue. Venue adapters leave those null; the platform stamps them at book admission. |
Trading events
The provider/service stream a single abstract TradingEvent hierarchy. TimestampUtc
and a monotonic Sequence are on the base; switch on the concrete type to react.
| Type |
Carries |
TradingEvent (abstract) |
DateTime TimestampUtc, long Sequence, string? OriginProviderKey (virtual, null on the base: the key of the trading provider the event is about, which is its account's provider or the provider it names; every event type below overrides it). |
AccountSnapshotEvent |
Account Account. |
AccountRemovedEvent |
AccountId AccountId. The provider no longer hosts the account (closed at the venue, or withdrawn on stop, failed start or rolled-back start). It never states the account's positions flat. |
OrderCancelRejectedEvent |
AccountId AccountId, OrderId OrderId, string Reason. |
OrderModificationRejectedEvent |
AccountId AccountId, OrderId OrderId, OrderModification Modification, string Reason. The order keeps its previous price and quantity; the event carries what was attempted. |
FlattenRefusedEvent |
AccountId AccountId, string? Instrument (null = the whole account), string Reason. The venue refused and the position is still open. |
RiskRejectionEvent |
AccountId AccountId, OrderRequest Request, RiskRejection Rejection. The platform's risk layer refused the placement before it reached the venue. |
OrderUpdateEvent |
Order Order. |
FillEvent |
AccountId AccountId, Fill Fill. |
PositionUpdateEvent |
Position Position. |
BalanceSnapshotEvent |
AccountId AccountId, IReadOnlyList<AccountBalance> Balances. |
ConnectionStateEvent |
string ProviderKey, bool Connected, string? Reason. |
BooksSyncedEvent |
string ProviderKey, IReadOnlyList<Position> Positions. Raised by a provider exactly once per connect, after it has published every open order and position it holds (and after its ConnectionStateEvent for that connect). Positions is the book it vouches for, captured when it raised the event: the provider's full-book statement. From then on a position the provider does not state is a position it does not hold. See Books-synced. |
AccountBookSyncedEvent |
AccountId AccountId. Raised by a provider for an account that came up after this connect's BooksSyncedEvent (created or enabled while connected), once it has announced the account with an AccountSnapshotEvent and published every open order and position the account holds. From then on a position the provider does not state on this account is a position it does not hold. A provider that cannot read the account's whole book must not raise it. An account that only appears, with an AccountSnapshotEvent and nothing more, is not vouched for. |
ReplayCompletedEvent |
string ProviderKey. Raised by a provider that cannot raise BooksSyncedEvent for a connect: it has stated every order and position it could read, and it does not vouch that what it did not state is not held (a venue whose lists can be cut short, or whose positions are added up from its own fills). At most once per connect, after that connect's ConnectionStateEvent, and never together with a books-synced for the same connect. The broker facts read the provider Current from then on, so what waits for a current venue (a strategy's entries, the ATM's management of a trade) goes on. Nothing is retired on its word: a position or working order the provider did not restate stays as it was last stated, and proof of a flat position still needs the provider's own flat statement. |
AccountStateResetEvent |
AccountId AccountId. Raised by a provider whose account was reset outside trading, such as the simulator's account reset. Every order and position stated for the account before it belongs to a history that no longer exists; the provider restates the account after it. |
ProviderLifecycleEvent |
string ProviderKey, ProviderFactsStatus Status. Raised by the trading service, never by a provider, when how far a provider's statements can be trusted changes: attached, books synced, disconnected, detaching, removed. It follows the event that caused the change. See Broker facts. |
Broker facts: SabrTrader.Pipeline.Venues.Trading.Facts
A venue-agnostic read model of what the brokers last said: every order and position each attached
trading provider has stated, the most recent execution per position, and how far each provider's
statements can be trusted. It holds statements only: positions come from what the broker stated, and a
position the broker has not stated is unknown until it does. The trading service owns the one writer and
feeds it every provider event on its emit pump, before any decorator or subscriber sees it, so a hidden account still has its facts. The host serves the read
side as IBrokerFacts to its own order-management components (the ATM reads it). It is
documented here because a trading provider's events are all it is built from: what your provider raises
decides what it can prove (see Books-synced).
| Type |
Members |
IBrokerFacts |
OrderFactSlot? FindOrder(OrderId), OrderFactSlot? FindByBroker(AccountId account, string brokerOrderId) (account-scoped: broker ids are not promised unique across the accounts of one login), PositionFactSlot? FindPosition(PositionScopeKey), int CopyWorkingOrdersOn(PositionScopeKey scope, Span<OrderFactSlot> into) (copies the slots of the orders the broker states working on that account and venue instrument at the moment of the call and returns how many there are; when that exceeds into.Length only that many were copied, so ask again with room for all; only orders the provider's current session stated are listed, so at its books-synced boundary an order the reconnect's replay did not restate drops out; allocates nothing, any thread); OrderFactSlot? PinOrder(OrderId), PositionFactSlot PinPosition(PositionScopeKey), Unpin(OrderFactSlot), Unpin(PositionFactSlot); ProviderFactsStatus StatusOf(string providerKey); bool PositionSettled(PositionScopeKey) (no execution on the position within its settle window, below); bool IsFlatAndClear(PositionScopeKey key, out TimeSpan settlesIn) (proven flat: the broker states the position flat, no order works on the scope, and it is settled; while a flat statement is younger than the last execution it answers false and settlesIn says when to ask again, otherwise TimeSpan.Zero); bool BookShowsNoPosition(PositionScopeKey) (the provider's own book shows no position on the scope: it stated its whole book for the account, at this connect's books-synced boundary or with an AccountBookSyncedEvent, its status is Current, and it has stated no position and reported no fill on the scope after that; its silence is then its word that nothing is open there. False when that word is missing: the provider ended its replay with a ReplayCompletedEvent, the account was announced after the boundary and its own book was never stated, or the scope has a position statement or a fill, in which case IsFlatAndClear decides. It says nothing about working orders: check CopyWorkingOrdersOn too); bool PositionOnTheWay(PositionScopeKey) (the position and its fills do not tell one story yet, so it is not a position to size an order from: true while the moves the venue stated and the fills heard do not add up, whichever arrived first, for at most the provider's settle window from the moment the two parted; a confirmed book or pull ends it at once; false when the table holds no slot for the position); Task<bool> RefreshPositionAsync(PositionScopeKey, ct); IDisposable Subscribe(IBrokerFactsObserver). Lookups are lock-free and allocate nothing. A consumer pins the slots it depends on (pinning resolves and pins in one step, so a slot dropped a moment ago is never revived) and reads them with Read(). A slot found without a pin is for a quick look: it may be dropped at any time, so never hold one. |
BrokerFactsTable : IBrokerFacts
|
The one writer. BrokerFactsTable(Func<string, ITradingProvider?> providerOf, Func<Action, bool> postToPump, Action<string, ProviderFactsStatus> statusChanged, Action<string>? diagnostic = null, TimeProvider? clock = null): providerOf finds the attached provider a pull goes to, postToPump runs work on the emit pump, statusChanged is told of each status change (the service publishes it as a ProviderLifecycleEvent), diagnostic hears an observer's fault, a skipped statement or a refused pull, and clock times an execution's arrival against the settle window (the system clock when null). Writers, on the pump only: Apply(TradingEvent evt, IReadOnlyList<Order> openOrders), OnAttached(providerKey), OnDetaching(providerKey), OnRemoved(providerKey). The trading service also calls Order? RequestSent(OrderId orderId, OrderRequestKind kind) when a cancel or a modify of an order goes to its venue, and Order? RequestNoLongerOut(OrderId orderId, OrderRequestKind kind) when that request is no longer out (a refused cancel, an answered change): each returns the order as it reads from then on, to be told to consumers, or null when it reads the same. That is what states an order PendingCancel or PendingModify. static TimeSpan SettleWindowOf(ITradingProvider? provider) is how long after an execution the provider's position stream can still be catching up with it: its declared IPositionPropagationDelay, never less than MinimumSettle, also for a null provider. As an IBrokerFacts it answers BookShowsNoPosition and PositionOnTheWay too. const int UnpinnedTerminalRetention = 1024: terminal order statements nothing pins that stay readable, oldest evicted first. static readonly TimeSpan MinimumSettle (10 seconds): the shortest settle window, below. Some venues deliver an execution before any statement of its order: the table parks such an execution and applies it when the order's first statement arrives. const int ParkedExecutionRetention = 256 caps how many wait (oldest dropped and reported first), static readonly TimeSpan ParkedExecutionMaxAge (60 seconds) is how long one waits before it is dropped and reported, and long UnkeyedExecutions counts the executions that arrived before their order was stated. IReadOnlyList<Order> WorkingOrders is every order stated working now, as of the last statement the table took, in first-stated order and each with its latest statement: it is what the trading service returns as OpenOrders. It is republished on the emit pump by every writer that changes an order or which orders work (a statement, a reset, a removal, a books-synced restatement, an attach, a provider removal), before the event that caused it reaches any subscriber; executions, positions and ticks never republish it. Reading it is one volatile read on any thread, with no lock and no allocation. Readers run on any thread without a lock; writing a statement to a slot that exists allocates nothing. |
OrderFactSlot · OrderFact
|
OrderId OrderId, AccountId Account, Order? Current, bool IsPinned, OrderFact Read(); and the readonly record struct (Order? Order, long AppliedVersion), one whole reading from one write. A pinned order's terminal statement stays readable until it is unpinned; unpinned and terminal, it joins the bounded retention. |
PositionFactSlot · PositionFact
|
PositionScopeKey Key, Position? Current (null = unknown, never flat), bool IsPinned, PositionFact Read(), int ReadExecutions(long afterSequence, Span<ExecutionFact> into, out bool lost), const int ExecutionRetention = 16; and the readonly record struct (Position? Position, bool Confirmed, long AppliedVersion, long ExecutionCount, decimal LastExecutionQuantity, long LastExecutionVersion, bool CaughtUp, decimal ImpliedQuantity, decimal AheadOfFills). LastExecutionQuantity is signed (positive bought, negative sold) and corroborates a change of the position; it never changes the position number. A statement applied at a higher version than LastExecutionVersion was stated after that execution. CaughtUp is true when the stated position accounts for every fill heard, including when the venue has stated no position and no fill has been heard. ImpliedQuantity is the signed quantity the fills imply; it is not the position, which stays the venue's statement. AheadOfFills is the moves the venue has stated minus the fills heard, signed (bought positive): zero when the position and the fills tell one story, and it only means something inside the provider's settle window, so ask IBrokerFacts.PositionOnTheWay. ReadExecutions copies the executions numbered after afterSequence that the slot still keeps (the last ExecutionRetention), oldest first and all from one write, into into (give it room for ExecutionRetention) and returns how many it copied; lost is true when some executions after afterSequence are no longer kept, so their orders are unknown. |
ExecutionFact |
readonly record struct (OrderId Order, decimal SignedQuantity, long Version, long Sequence): one execution on a position, as ReadExecutions hands it out. SignedQuantity is positive bought, negative sold; Version is the provider's arrival version; Sequence numbers it on the position from 1 (the position's execution count right after it). It lets a consumer tell its own orders' executions from anyone else's by order rather than by counting, since venues deliver an execution before or after its order statement. |
PositionCatchUp |
readonly record struct: whether the position a venue has stated accounts for the fills heard, and the signed quantity those fills imply. static PositionCatchUp Initial (no fill heard and no position stated), bool CaughtUp (no fill is waiting for a position statement), decimal Implied (the signed quantity the fills imply; while CaughtUp, the quantity the venue stated); PositionCatchUp OnFill(decimal signedGrowth) (a fill, bought positive; zero counts nothing), PositionCatchUp OnStatement(decimal quantity, out bool differs) (the venue stated a quantity; differs is true when a fill was waiting and this statement is not that fill and not a re-mark still on the way to it), PositionCatchUp Accept(decimal quantity, out bool differs) (a confirmed book or a written pull: the quantity is the position). Each call returns the next value. It is the arithmetic behind PositionFact.CaughtUp and PositionFact.ImpliedQuantity; no other SDK member returns or takes it. |
OrderRequestKind |
enum: Cancel (cancel the order), Modify (change the order's price, quantity or time in force). What the trading service asked a venue to do with an order it holds: the argument of BrokerFactsTable.RequestSent and RequestNoLongerOut. |
ProviderFactsStatus |
enum: Absent (no provider with this key is attached), Replaying (attached or reconnected; neither books-synced nor replay-completed raised yet for this connect), Current (connected, and the provider ended its replay for this connect: with a BooksSyncedEvent, so what it did not state it does not hold, or with a ReplayCompletedEvent, so it vouches for nothing it did not state), Disconnected (the provider said its connection is down; its last statements may be stale), Detaching (ITradingService.BeginDetach was called). A missing statement means "not held" only under Current reached through a BooksSyncedEvent; under every other status, and under Current reached through a ReplayCompletedEvent, it means nothing. |
IBrokerFactsObserver |
OnOrderFact(OrderFactSlot), OnPositionFact(PositionFactSlot), OnExecution(PositionFactSlot), OnProviderStatus(string providerKey, ProviderFactsStatus), OnAccountReset(AccountId), OnAccountRemoved(AccountId). Called on the emit pump right after the slot is written, before any decorator or UI subscriber sees the event. Each call must be O(1), allocate nothing and never block: hand the change to your own sequence and return. A throw is caught and reported, never passed on. |
When a statement is proof
-
Arrival versions order one provider's statements. Each write records the provider's arrival
version (
AppliedVersion). It is the only ordering a consumer may rely on: a timestamp from one
feed is never compared with a timestamp from another.
-
Confirmed. A position statement is confirmed when it was written at the provider's books-synced
boundary (present in the book it published, or absent from it) or by an on-demand pull, and nothing has
changed it since. A statement of another quantity, an execution (or a fill stated on its order), a
connect, a disconnect or an account reset ends it; a re-mark of the same quantity keeps it. It is proof only while
StatusOf the provider reads
Current.
-
Flat by absence happens once per connect. At a books-synced boundary a pinned position absent from
the published book reads flat and confirmed, and an unpinned slot that still states a position the book no
longer holds goes back to unknown. Only a provider that is replaying reaches a boundary: one raised while it
is disconnected, detaching or removed proves nothing and is ignored. There is no other flat by absence;
otherwise a position is flat only when its provider says so.
-
The settle window. A provider's own position stream can trail a fill. Neither a boundary nor a pull
confirms a position whose last execution arrived less than
max(IPositionPropagationDelay, BrokerFactsTable.MinimumSettle) ago: the book's statement is
written unconfirmed, a pinned position the book omits keeps its statement (unconfirmed) instead of reading
flat, and RefreshPositionAsync returns false, so ask again later.
-
Pulls.
RefreshPositionAsync asks the provider's IPositionRefresh facet and
writes the answer confirmed. It returns false when the provider is not attached, has no such facet, is
inside the settle window, or stated another quantity (or had an execution on the position) while the pull
was in flight: that later statement stands. A failed pull throws what the provider threw. Never wait on it
from the emit pump: the write it waits for runs there.
-
Providers and accounts that go away. When a provider is removed, its statements nothing pins are
dropped and pinned ones stay, read against its status. On
AccountStateResetEvent or
AccountRemovedEvent, what nothing pins on that account goes back to unknown; a pinned slot keeps
its statement (a pinned position unconfirmed) for the observer to settle.
ATM: SabrTrader.Pipeline.Venues.Trading.Atm
An AtmStrategy describes a multi-bracket exit plan (per-bracket stop/target, auto-breakeven,
auto-trail, optional time exit). The host's ATM engine consumes it; a strategy hands one to Strategy.UseAtm, and a
plugin that saves templates for the trader validates them with AtmStrategyAuthoring. The engine,
its order gateway and the chart host below are the host's ATM runtime: they are listed so the types you meet
in its events and exceptions have a definition.
| Type |
Members |
AtmStrategy |
record (string Name, AtmParameterUnit Unit, IReadOnlyList<AtmBracket> Brackets, AtmStopStrategy StopStrategy, AtmTimeExit? TimeExit = null); init int AtrPeriod (Wilder ATR lookback, default 14), init AtmAtrTimeframe? AtrTimeframe (the bars the ATR is measured on; null on a template without ATR legs); decimal TotalQuantity, bool RequiresAtr (any side of any bracket resolves to Atr), bool RequiresPipSize, bool UsesAdaptiveBrackets, void Validate(). Validate checks structure only; an ATR template without a timeframe passes it and refuses to arm, and AtmStrategyAuthoring.Problems reports it. The timeframe is never defaulted, because a guessed one would change the trader's risk. |
AtmAtrTimeframe · AtmAtrTimeframeUnit
|
record (AtmAtrTimeframeUnit Unit, int Value), for example new AtmAtrTimeframe(AtmAtrTimeframeUnit.Minute, 5); void Validate() throws ArgumentException for an undefined unit or a value below 1. It belongs to the template, so the same template sizes the same stop from every chart or SuperDOM that places the trade; the ATR series is always the traded instrument's own contract. JSON form {"Unit":"Minute","Value":5}. Enum Minute, Hour, Day, Tick (one bar per Value trades): time and tick bars only, since a Renko or range bar has no fixed size and would size a different stop on every chart. |
AtmStrategyAuthoring |
static IReadOnlyList<string> Problems(AtmStrategy strategy, bool adaptiveBracketsEntitled, bool pipsAvailable): every reason, one plain sentence each, why the strategy may not be saved in this session; empty when it may. It covers the structural rules of Validate, set-ups that run but can never do what they were built for (a breakeven or trail step that never moves a stop before every target has filled, duplicate trail steps, an ATR template with no timeframe), ATR or Risk-Reward units without the entitlement, and Pips without a forex instrument. Stops and targets in units other than ticks are only known at the fill, so they are assumed to leave room. The ATM editor, the AI tools and anything else that authors a template go through it; loading and trading stay on Validate. |
AtmBracket |
record (decimal Quantity, decimal StopLoss, decimal ProfitTarget); bool IsRunner, init decimal FallbackStopTicks, AtmParameterUnit? StopLossUnit, AtmParameterUnit? ProfitTargetUnit; AtmParameterUnit EffectiveStopLossUnit(AtmParameterUnit strategyUnit), AtmParameterUnit EffectiveProfitTargetUnit(AtmParameterUnit strategyUnit), Validate(int index). |
AtmStopStrategy |
record (AtmStopOrderType StopOrderType, decimal StopLimitOffsetTicks, AtmAutoBreakeven? AutoBreakeven, AtmAutoTrail? AutoTrail); static StaticMarket, Validate(). |
AtmAutoBreakeven |
record (decimal ProfitTrigger, decimal Plus); Validate(). |
AtmAutoTrail |
record (IReadOnlyList<AtmAutoTrailStep> Steps); const int MaxSteps = 3, Validate(). |
AtmAutoTrailStep |
record (decimal ProfitTrigger, decimal StopLoss, decimal Frequency); Validate(int index). |
AtmTimeExit |
record (TimeSpan CloseAt, string TimeZoneId); Validate(). |
AtmParameterUnit |
enum: Ticks, Currency, Percent, Price, Atr, RiskReward, Pips. |
AtmStopOrderType |
enum: StopMarket, StopLimit, Simulated. |
IAtmStrategyStore |
IReadOnlyList<AtmStrategy> Load(), void Save(...), event EventHandler? Changed. |
IAtmDefaultsStore |
Load(), Save(...), string? GetDefault(string instrument), void SetDefault(string instrument, string? strategyName). |
IPositionManagementRegistry |
IDisposable Claim(...), IDisposable? TryClaim(..., Func<ManagedInstrument,bool> conflictsWith), IDisposable TakeClaim(..., Func<ManagedInstrument,bool> displaceIf), bool IsClaimed(...), event Action? Changed. TakeClaim releases every conflicting claim by another owner first: the trade copier uses it on accounts it mirrors into, because the copier always wins. Displaced owners observe Changed and must drop their token. |
ManagedInstrument |
readonly record struct (string? CanonicalKey, string NativeSymbol, bool ExclusiveExits = false). ExclusiveExits = the claimant manages EVERY exit from the account position, so it cannot coexist with another such claimant (a strategy run claims exclusively; a manual chart engine does not). |
PositionManagementConflictException |
(AccountId Account, string Instrument): thrown when an exclusive claim is refused because another exclusive claim is live on the scope. Let it propagate: the run faults visibly instead of trading unprotected or double-bracketed. |
ManualLegPlacement |
enum: Placed, NoOpenPosition, ManagedElsewhere, WrongSideOfMarket, Refused. Anything but Placed placed nothing and the trader has already been told why; the surface only snaps back. |
PendingEntryBracket · PendingEntryBrackets
|
readonly record struct (decimal? StopPrice, decimal? TargetPrice) with IsEmpty, With(bool isStop, decimal? price), IsOnProtectiveSidesOf(OrderSide entrySide, decimal entryPrice); and the collection keyed by entry OrderId (SetLeg, Shift, TryGet, TryTake, Remove, Clear, event Action<OrderId>? Changed). Nothing rests at the venue for these: they are intent, drawn as inactive lines, and become real engine-owned protective orders the moment the entry fills, and are forgotten when it dies unfilled. |
IDynamicStopLevelSource |
decimal? CurrentLevel, string Description: a price level (an indicator plot, for example) the ATM's stop follows once attached with an offset in ticks; null while the level is not available. |
Bracket presets: SabrTrader.Pipeline.Venues.Trading.Brackets
A reusable stop/target offset preset (in price, ticks or pips) plus the math to turn it into absolute bracket
prices for a given entry and side.
| Type |
Members |
BracketPreset |
record (string name, BracketOffsetUnit unit, decimal? stopLossOffset, decimal? takeProfitOffset); init Name, Unit, StopLossOffset, TakeProfitOffset. |
BracketOffsetUnit |
enum: Price, Ticks, Pips. |
BracketPriceResult |
readonly record struct (decimal? StopLossPrice, decimal? TakeProfitPrice). |
BracketPresetMath |
static: BracketPriceResult ComputeBracketPrices(decimal entryPrice, OrderSide side, BracketPreset preset, decimal tickSize, decimal pipSize = 0m). |
Risk validation: SabrTrader.Pipeline.Venues.Trading.Risk
An IRiskValidator inspects an order against RiskLimits and a RiskSnapshot
and returns a RiskRejection (one of the typed subtypes) or null to allow it.
| Type |
Members |
IRiskValidator |
RiskRejection? Validate(OrderRequest, AccountId, RiskSnapshot), RiskRejection? ValidateModification(OrderRequest currentOrder, decimal newQuantity, AccountId, RiskSnapshot). |
RiskLimits |
record: decimal? MaxPositionSize, MaxDailyRealizedLoss, int? MaxOpenOrders, MaxOrderQuantity, MaxTotalLoss, DailyProfitLock, int? MaxConsecutiveLosers. |
RiskSnapshot |
record (Account Account, Position? CurrentPosition, int OpenOrderCount, AccountRiskState? RiskState = null, RiskProfileRules? ProfileRules = null, RiskSchedule? ProfileSchedule = null, DateTime? LocalNow = null, decimal WorkingBuyQuantity = 0m, decimal WorkingSellQuantity = 0m). WorkingBuyQuantity and WorkingSellQuantity are the quantity still resting in working orders on the instrument, per side: they drive the position-size check for an order that adds to the position. ProfileRules, ProfileSchedule and LocalNow carry the armed profile's rules and schedule plus the local clock they are judged against: the validator reads what the caller put here rather than taking the registry's lock again on the order path. |
AccountRiskState |
record (int ConsecutiveLosers, DateOnly TradingDate, AccountRiskDailyState? DailyState = null); static ForDate(DateOnly). |
RiskRejection (abstract) |
string Reason; subtypes PositionSizeExceeded, DailyLossLimitHit, OpenOrderCountExceeded, OrderQuantityExceeded, TotalLossLimitHit, DailyProfitTargetReached, ConsecutiveLossLimitHit, VenueNotConnected, FlattenAllRunning (raised by the trading service itself, not by the validator, while a Flatten All runs: no order is placed on any account until every close has ended and been confirmed; cancel, modify and flatten are never refused for it, and the block ends by itself), AccountHardLocked (carries RiskDailyLockReason LockReason), DailyTradeLimitHit (TradesToday, Limit), DailyTrailingDrawdownHit (PeakTotalPnL, CurrentTotalPnL, DrawdownUsed, Limit), TradingScheduleBlocked. Switch on the type, never on the message. |
RiskProfile |
record (RiskProfileId Id, string Name, RiskLimits Limits, RiskProfileRules Rules, RiskSchedule Schedule); bool HasAnyCap. A named, reusable policy. |
AccountRiskDailyState |
record (DateOnly LocalTradingDate, int TradesToday, decimal PeakTotalPnL, bool Locked, RiskDailyLockReason? LockReason, DateOnly? LockLocalDate); IsLockedFor(DateOnly), static ForDate(DateOnly). The running day: what the trade-count and trailing-drawdown rules read. |
RiskDailyLockReason |
enum: Manual, DailyLossLimit, DailyProfitTarget, ConsecutiveLosses, DailyTradeLimit, DailyTrailingDrawdown, SingleTradeLoss, SingleTradeProfit, TotalLossLimit: why the account is locked for the day. |
RiskProfileRules |
record (int? DailyTradeLimit = null, decimal? DailyTrailingDrawdown = null, decimal? SingleTradeLossLimit = null, decimal? SingleTradeProfitTarget = null); HasAnyCap. Null disables that rule, like RiskLimits. |
RiskSchedule · RiskTimeWindow
|
record of the seven day flags AllowSunday, AllowMonday, AllowTuesday, AllowWednesday, AllowThursday, AllowFriday, AllowSaturday plus IReadOnlyList<RiskTimeWindow> TimeWindows; IsDayAllowed(DayOfWeek), IsTimeAllowed(TimeOnly), IsTradingAllowed(DateTime localNow), static AllDaysAllDay. A window is (TimeOnly Start, TimeOnly End) with Contains(TimeOnly). Outside the schedule, placement is refused with TradingScheduleBlocked. |
IAccountRiskStateRegistry |
AccountRiskState GetState(AccountId), event Action<AccountId>? StateChanged. |
Commissions: SabrTrader.Pipeline.Venues.Trading.Commissions
An ICommissionModel turns a CommissionContext (symbol, qty, price, side,
liquidity) into a CommissionResult. RateCommissionModel covers the common
rate-based cases. A fill is priced by the first of: the venue's own fee on the fill
(Fill.CommissionIsAuthoritative), the venue's published rate for the account
(IVenueCommissionRateSource), the trader's commission settings, zero. See
Venue-reported fees.
| Type |
Members |
ICommissionModel |
CommissionResult Compute(in CommissionContext ctx). |
RateCommissionModel |
RateCommissionModel(CommissionRate rate); CommissionRate Rate, Compute(in ctx). |
CommissionModels |
static: readonly ICommissionModel Zero, ICommissionModel Flat(decimal perUnit). |
CommissionRate |
record (CommissionBasis basis, decimal value, decimal minFee = 0m, decimal maxFee = 0m, decimal? makerValue = null, decimal? takerValue = null, CommissionAccrual accrual = PerSide); static Zero. |
CommissionContext |
readonly record struct (string Symbol, decimal Quantity, decimal Price, decimal Multiplier, OrderSide Side, Liquidity Liquidity, string Venue, decimal PriorOrderRawCommission = 0m, string? InstrumentKey = null). |
CommissionResult |
readonly record struct (decimal Amount, decimal CumulativeRawCommission); static Zero. |
CommissionBasis |
enum: PerUnit, PerOrder, PercentNotional, BasisPoints. |
CommissionAccrual |
enum: PerSide, PerRound. |
Liquidity |
enum: Unknown, Maker, Taker. |
IVenueCommissionRateSource |
Optional ITradingProvider facet for a venue that publishes commission rates per product and account (Rithmic's FCM-configured RMS rates are the model case), probed with AsCapability<IVenueCommissionRateSource>(): CommissionRate? TryGetVenueCommissionRate(AccountId account, string productRoot) (the venue's rate for one product root, such as "MNQ", on one account; null when it published none, and the trader's settings price the fill), IReadOnlyList<VenueCommissionRate> VenueCommissionRates(AccountId account) (every published rate for the account, for display; empty when none). Must be thread-safe: the commission layer reads it from the trading event stream while your session updates it. |
VenueCommissionRate |
readonly record struct (string ProductRoot, CommissionRate Rate): one published rate, as the venue configured it. |
Client-side OCO: SabrTrader.Pipeline.Venues.Trading.Oco
For venues without NativeOco, ClientSideOcoCoordinator emulates one-cancels-other:
it places the legs through an IOcoLegGateway, and when one fills it cancels (or resizes) the
others, with retry and optional durable intent (IOcoIntentStore) so a group survives a restart.
A provider that enforces groups this way for legs placed one by one through PlaceAsync declares
EnforcesOcoGroups. What a REJECTED member does depends on the group's OcoGroupKind:
in an Alternatives group it cancels its siblings, so the group is never half-armed; in a
ProtectiveExits group its siblings keep working and the coordinator logs the rejection, leaving the
owner to re-place the refused leg. In an EntryPair group the first fill of any size cancels
the other member, and a rejected member breaks the group as in Alternatives. The store receives a
working leg's membership once, not on every restatement of the leg.
Who keeps a venue's groups decides who links two orders after they were placed
(ITradingService.LinkEntryPairAsync). For a provider with a leg port the trading service builds
one ClientSideOcoCoordinator and links through it. A provider that declares
EnforcesOcoGroups keeps its groups itself and takes the link through
IOcoGroupLinking. A venue with neither refuses with
OcoLinkRefusal.VenueCannotLink.
| Type |
Members |
ClientSideOcoCoordinator |
ClientSideOcoCoordinator(IOcoLegGateway gateway, int retryAttempts = 3, Action<string>? log = null, IOcoIntentStore? store = null, Action? linksChanged = null) (linksChanged is told when which orders are linked changed without a statement of those orders: two working orders were linked, a reconnect re-armed its groups, or an entry pair lost one order; it is called outside the coordinator's lock, on the thread that made the change); Task<OcoPlacement> PlaceAsync(OcoRequest, AccountId, ct), Task OnOrderUpdate(Order), void Register(OrderRequest request, OrderId) (registers a leg placed through PlaceAsync into the group its OcoGroupId names and records its OcoGroupKind; register with this overload so the kind is never dropped), void Register(string? groupId, OrderId orderId, OcoGroupKind kind = Alternatives) (a provider that re-arms its own groups reads the kind from the store with IOcoIntentStore.KindOf), Task<OcoLinkRefusal?> LinkAsync(string groupId, OcoGroupKind kind, OrderId first, OrderId second) (links two orders that already work at the venue into the group groupId of that kind: from then on they are enforced exactly as two legs placed in that group would be; null when linked, otherwise why not, with nothing changed; each order's current statement is then handled once, so the membership is stored and a leg that filled while the link was being made takes its sibling down), bool IsInGroupOf(OrderId order, OcoGroupKind kind) (true while the order is a member of a group of that kind), void Forget(OrderId), void Rearm(IReadOnlyList<Order> reconciledOpenOrders), Task SweepOrphansAsync(IReadOnlyList<Order> reconciledOpenOrders, Func<AccountId, string, bool> isPositionKnownFlat), void Reset(). PlaceAsync rolls back the legs already placed when one fails and returns Failed, never with NothingRests: a leg that threw may have reached the venue and the rollback cancels are unconfirmed. Rearm restores persisted membership after a reconnect and never cancels; SweepOrphansAsync, called after it, cancels a partnerless survivor only when isPositionKnownFlat returns true for its account and instrument (return true only when the provider positively knows the position is flat) and otherwise leaves it armed and logs it. |
IOcoLegGateway |
Task<OrderId> PlaceLegAsync(OrderRequest leg, AccountId, ct), Task<bool> CancelLegAsync(OrderId, ct), Task<bool> ResizeLegAsync(OrderId, decimal newQuantity, ct), bool TryGetOrder(OrderId, out Order). |
IOcoIntentStore |
IReadOnlyDictionary<string, IReadOnlyList<string>> LoadGroups(string accountId), void SaveMember(string accountId, string groupId, string brokerOrderId, OcoGroupKind kind = Alternatives) (kind is the group's as a whole: the last one saved stands), OcoGroupKind KindOf(string accountId, string groupId) (what the members of the group are to each other, as saved with them; Alternatives for a group saved without a kind or not known at all), void RemoveMember(string accountId, string brokerOrderId). Members are keyed by the venue's own order id, the only identity that survives a restart or a reconnect. An implementation is safe for concurrent calls and idempotent, and throws when a durable write fails; the coordinator catches and logs that. |
NullOcoIntentStore |
no-op IOcoIntentStore; static Instance. KindOf answers Alternatives. |
VenueStateOcoIntentStore |
VenueStateOcoIntentStore(IVenueStateStorage storage, Guid profileId): the durable IOcoIntentStore for venue plugins, persisted through the host's venue state storage with LoadGroups, SaveMember, KindOf and RemoveMember. A venue session builds one for its profile and hands it to its trading provider, which hands it to the trading service through IOcoCoordinatorHost.OcoStore. The state is stored per profile, so two concurrent logins never write the same file. A write that fails throws. Thread-safe. |
IOcoCoordinatorHost |
Optional provider facet, probed with AsCapability<IOcoCoordinatorHost>(): a provider whose groups the trading service enforces and which feeds the service's coordinator itself. void Adopt(ClientSideOcoCoordinator coordinator): the trading service hands over its coordinator for this provider once, when the provider is attached and before any of its statements is heard. From then on the provider calls OnOrderUpdate for every order statement on the thread that wrote it, after it released its locks and flushed its outbox (ProviderEventOutbox.ObserveOrders does this), while the service still registers each placed leg. IOcoIntentStore? OcoStore (default null): the durable store the service builds this provider's coordinator over, so a group survives a restart; the provider restores the groups with Rearm once it has read its orders back. A provider with a leg port and without this facet is fed by the trading service when each statement is delivered. |
IOcoGroupLinking |
Optional provider facet, probed with AsCapability<IOcoGroupLinking>(), for a provider that keeps its one-cancels-other groups itself (it declares EnforcesOcoGroups, so the trading service holds no coordinator for it; the built-in simulator is one). OcoLinkRefusal? LinkEntryPair(string groupId, OrderId first, OrderId second): links two of its working orders as the entry pair groupId, so the first to fill cancels the other; null when linked, otherwise why not, with nothing changed. bool IsInEntryPair(OrderId order): true while the order works as a member of an entry pair. A provider whose groups the trading service keeps needs neither. |
OcoLinkOutcome · OcoLinkRefusal
|
readonly record struct (OcoLinkRefusal? Refusal) with bool IsLinked (true when Refusal is null: the first of the two orders to fill cancels the other) and factories static Linked and static Refused(OcoLinkRefusal reason): what ITradingService.LinkEntryPairAsync returns. Enum OrderNotWorking (one of the orders no longer works: it filled, was cancelled, or is not known), NotOnePair (the same order twice, or orders on different accounts or instruments), NotRestingOrders (one of them is a market order, which does not rest), AlreadyLinked (one of them is already in a one-cancels-other group: a linked pair, or an ATM's stop and target), VenueCannotLink (the account's venue has no owner for a link made after the orders were placed). Nothing changed at the venue in any of these cases: both orders keep working as they were. |
Cancel-replace modify: SabrTrader.Pipeline.Venues.Trading.CancelReplace
For a venue with no native modify. A modify there cancels the venue order and places a replacement, so one
platform OrderId spans several venue orders, and each venue order reports only its own cumulative
fill. CancelReplaceSequence holds the one decision tree for every such venue and
CancelReplaceLedger<TReport> keeps every revision's fills counted once. The provider
implements ICancelReplaceHost<TReport> over its own wire and book, keeps one ledger beside its
book, and calls RunAsync on CancelReplaceSequence from its ModifyAsync. The host
application implements none of these types. A provider whose venue modifies an order in place needs none of
them.
| Type |
Members |
CancelReplaceSequence |
static. Task<bool> RunAsync<TReport>(ICancelReplaceHost<TReport> host, OrderId id, Order current, OrderRequest effective, CancellationToken ct) runs one cancel-replace: current is the order as the book held it when the modify was merged, and effective is that merge (see OrderModification.TryApplyTo), validated and venue-snapped, whose quantity is the order's total after the change. True when the order rests at the venue as modified. False when the modify was refused or could not complete: the reason is in the log, and on the order when the modify ended it. It throws only the caller's cancellation, and only before anything was sent. The steps: claim the order (ICancelReplaceHost.TryOpen) before anything is sent; cancel the venue order, where a refusal ends the modify with the order as it was; read back what the venue holds of it, and place nothing while the venue still holds it working or while neither the cancel nor the read proved it gone; place nothing for an order that filled completely during the cancel; size the replacement as the total minus everything the order has filled; place it. An accepted replacement continues the order. A refused one states the order Cancelled with the reason. A lost placement answer marks the pair unresolved and the venue's session orders decide. Every statement is delivered before the modify answers. Task WhenNoPairInFlightAsync(Func<Task?> readUnderGate) and WhenNoPairInFlightAsync(Func<Task?> readUnderGate, CancellationToken ct, Action? onWaiting = null): the wait of a cancel, or a flatten's cancel, issued while a cancel-replace of the same order may be in flight. readUnderGate runs under the provider's book gate and returns null when the cancel knows what to do, else CancelReplaceLedger.WhenSettled of the order; the helper awaits that task outside the gate and reads again, so a cancel never addresses a venue order a modify is superseding. onWaiting is invoked once when the caller starts waiting. Order LostByItsModify(Order latest, string reason, DateTime nowUtc): the order as it ends when its own change lost it, Cancelled with the reason and OrderEndCause.OwnModifyFailed. bool EndedWithoutAFill(in VenueOrderReport report): the venue order ended (cancelled, rejected or expired) without trading a lot. string ReplacementEndedUnfilledReason(OrderState? endedAs): the reason on an order whose replacement the venue took and then ended without a fill. Constants: ModifyFailedPrefix ("Modify failed:", how Order.RejectReason begins when a modify ended the order, so a consumer that manages protection re-places it), UnconfirmedReplacementEndedReason (starts with the prefix) and FlattenEndReason (deliberately without the prefix: nothing is placed again in the middle of a flatten). |
ICancelReplaceHost<TReport> |
Implemented by the provider that owns the order; called by CancelReplaceSequence. The venue half, in the venue's own wire shape: Task<VenueCancelAck> CancelAtVenueAsync(Order live) (the send passes the provider's send fence; a request that may have reached the venue without an answer is Ambiguous), Task<PostCancelInspection<TReport>> InspectAfterCancelAsync(Order live) (reads the cancelled venue order back; a venue that cannot be read answers Unknown, never Gone), Task<VenuePlacementAck> PlaceReplacementAsync(Order live, OrderRequest replacement, string clientReference) (places the replacement, already sized to what is left to work, with everything the venue needs to keep it the same kind of order), Task<IReadOnlyList<TReport>> FetchSessionOrdersAsync() (throws when the venue cannot answer; never an empty list for a read that failed), bool ListsTerminalOrders (true when that list holds filled, cancelled and rejected orders too, so an order absent from it never landed), bool IsReplacement(TReport report, Order live, OrderRequest replacement, string clientReference), TimeSpan PlacementProbeDelay (how long to wait before asking the venue about a placement whose answer was lost). The book half only forwards to the ledger under the provider's book gate, then writes and states what the ledger decided: bool TryOpen(OrderId id, Order expected, out CancelReplaceClaim claim), bool IsSessionOver(CancelReplaceClaim claim), void Abandon(CancelReplaceClaim claim, string? modifyEndReason), decimal? FilledSoFar(CancelReplaceClaim claim, TReport? lotsOnly, out bool sessionOver), void Fold(CancelReplaceClaim claim, TReport report), void CompleteReplacement(CancelReplaceClaim claim, OrderRequest effective, string venueOrderId), CancelReplaceCloseOutcome CloseAsNeverLanded(CancelReplaceClaim claim, string reason), void MarkUnresolved(CancelReplaceClaim claim, OrderRequest effective, string reason). Plus void Notify(VenueNotice notice) and void Log(string line). Every book method that changes an order stages its statement under the book gate and delivers it before it returns. |
ICancelReplaceReport |
Implemented by a venue's own order report type over its wire shape: string VenueOrderId (empty when the venue stated none), string? ClientReference (the client reference the venue echoes for that order, or null), VenueOrderReport Report (that venue order's own state and cumulative fill, counted from zero for each venue order). |
CancelReplaceLedger<TReport> |
sealed class, owned by the provider and called under the lock that guards its book. CancelReplaceLedger(Action<string>? diagnostic = null): diagnostic receives a line when a report is dropped that may carry money; it is called under the owner's lock and must only record the line. bool TryOpen(Order live, Order expected, string supersededVenueOrderId, long session, out CancelReplaceClaim claim) claims the order's next revision and opens its ledger before the cancel is sent; false, claiming nothing, while an earlier cancel-replace of the order is in flight or unresolved, when the order has moved on from expected, or while BlockNewPairs is held. bool Bind(OrderId orderId, string venueOrderId) (the replacement rests at the venue: the pair is over), TReport? Abandon(OrderId orderId, string? modifyEndReason = null) (no replacement placed: the ledger goes back to what it was and the claimed revision is given back), CancelReplaceCloseOutcome TryClose(OrderId orderId, string clientReference) (the replacement sent under that reference did not land), void MarkUnresolved(OrderId orderId, OrderRequest effective), bool TryGetUnresolved(OrderId orderId, out OrderRequest effective, out string clientReference), bool IsInFlight(OrderId), bool IsUnresolved(OrderId) (the replacement may rest at the venue under an id nobody knows yet: the owner refuses a cancel or a modify of the order), Task WhenSettled(OrderId) (completes when no cancel-replace of the order is in flight), IDisposable BlockNewPairs(AccountId account, string? instrument) (a flatten refuses every cancel-replace on the account, or on one instrument, until the scope is disposed), bool IsBlocked(Order order), bool TryTranslate(OrderId orderId, Order before, TReport report, out VenueOrderReport translated) (restates one venue order's report in terms of the platform order; an order that was never replaced passes unchanged), decimal UnfoldedLots(OrderId orderId, TReport superseded), string? ModifyEndReason(OrderId), Order CompleteFold(Order folded) (the last step of the owner's fold: returns the order to store and state), void Forget(OrderId) (the order ended), void Reset() (provider stop or dispose; every waiter is released). |
CancelReplaceClaim |
readonly record struct (long Session, Order Live, string ClientReference, decimal SupersededFilled) with OrderId OrderId (the id of Live): what TryOpen hands out. The sequence passes it back on every later book call, so the book knows which order, which pair and which session the call belongs to. |
VenueCancelAck · VenueCancelOutcome
|
readonly record struct (VenueCancelOutcome Outcome, string? Reason = null) with factories Cancelled(), AlreadyGone(), Refused(string reason), Ambiguous(string reason): a venue's answer to the cancel of one venue order. Enum VenueCancelOutcome.Cancelled (the venue accepted the cancel), AlreadyGone (the venue holds no working order under that id; this does not say the order was cancelled, it may have filled), VenueCancelOutcome.Refused (the venue refused, or nothing was sent: the order is as it was), VenueCancelOutcome.Ambiguous (the request may have reached the venue and its answer was lost). |
VenuePlacementAck · VenuePlacementOutcome
|
readonly record struct (VenuePlacementOutcome Outcome, string? VenueOrderId = null, string? Reason = null) with factories Accepted(string venueOrderId), Refused(string reason), Ambiguous(string reason): a venue's answer to the placement of one order. Enum VenuePlacementOutcome.Accepted (the venue accepted the order and answered its venue order id), VenuePlacementOutcome.Refused (nothing rests), VenuePlacementOutcome.Ambiguous (the order may or may not rest). |
PostCancelInspection<TReport> · PostCancelVenueState
|
readonly record struct (PostCancelVenueState State, TReport? Report = null, bool LotsOnly = false) with factories Unknown(), StillWorking(TReport? report = null), Gone(), Filled(TReport report), Settled(TReport report, bool lotsOnly = false): what a venue said about an order right after a cancel-replace cancelled it. It decides whether a replacement may be placed and how large it may be. Enum PostCancelVenueState.Unknown (the venue could not be read: never read as gone), PostCancelVenueState.StillWorking, PostCancelVenueState.Gone (the venue does not hold the order, or no longer lists it and cannot say why), PostCancelVenueState.Filled (filled completely: a replacement would trade the filled lots a second time), PostCancelVenueState.Settled (ended without a complete fill, possibly after a partial one). |
CancelReplaceCloseOutcome |
enum, the answer of TryClose and CloseAsNeverLanded: CancelReplaceCloseOutcome.Closed (the pair was still open and is closed: the owner states the order Cancelled with the reason), CancelReplaceCloseOutcome.Bound (the replacement was bound meanwhile: the order rests at the venue as modified and nothing is stated), NotOpen (the order ended, or the session did: nothing is stated). |
Provider lifecycle: SabrTrader.Pipeline.Venues.Trading.Lifecycle
Helpers a trading provider composes so that starts, stops and venue sends stay in step. The provider owns
and calls them; the host application does not.
| Type |
Members |
ProviderStartFlight |
sealed class: one start of a trading port at a time, never during a stop, and fenced by every stop that began after it. Task<bool> StartAsync(Func<long, CancellationToken, Task<bool>> start, CancellationToken ct) runs start once no stop is in progress, or joins the start already running and returns its answer or failure; start receives its attempt number. bool IsCurrent(long attempt) is true while no stop has begun after the start numbered attempt began: a start asks it, under the lock the stop's teardown takes, before every step a stop would have to undo (opening the book, announcing accounts, subscribing a stream), and stands down when it answers false. Task StopAsync(Func<Task> stop) fences every start that began before the call, then runs stop at once: a stop never waits for a start. void Stop(Action stop) is the synchronous form, for a Dispose or a stop on the venue's own thread. Thread-safe; the delegates run outside its lock. |
PlacementSendFence |
sealed class: the one fence every venue send of a trading provider passes, so nothing is sent after the session, or an account, was withdrawn, and every placement a withdrawal interrupts is handed back to be given a fate. The check and the start of the send are one step under the lock Close takes. A start calls bool TryOpen(long session, Func<bool> stillCurrent) with its ProviderStartFlight attempt number and a check that no stop overtook it. A placement calls bool TryRegister(OrderId id, AccountId account) when it creates its order (false: refuse the placement, and state and send nothing), bool TrySend<T>(OrderId id, Func<Task<T>> send, out Task<T>? sending) to send it (false, with nothing sent, when a close came first), and bool Settle(OrderId id) when the venue answered or the send failed (false when a close already reported the placement: the caller states nothing). A cancel, a modify or any other venue write goes through bool TrySendCommand<T>(AccountId account, Func<Task<T>> send, out Task<T>? sending). TrySend<TState, T> and TrySendCommand<TState, T> take a state argument handed to send, so the send needs no closure. The stop calls IReadOnlyList<InterruptedPlacement> Close() first, gives every placement it returns a fate, and only then withdraws the accounts. IReadOnlyList<InterruptedPlacement> CloseAccount(AccountId account) does the same for one account the venue withdraws while the session runs, and void OpenAccount(AccountId account) lets it through again when the venue announces it again. bool IsOpen and bool IsOpenFor(long session) take no lock. The fence's lock is the outermost one: call the members that take it holding no provider lock. The send delegates run under it, so they only start the I/O and return its task. Thread-safe. |
InterruptedPlacement |
readonly record struct (OrderId OrderId, bool Sent): a placement that was in flight when PlacementSendFence.Close or CloseAccount withdrew its session or account. One that was never sent is rejected on proof. One that was sent may rest at the venue: the provider's statement and notice say so. |
Session helpers
In SabrTrader.Pipeline.Venues.Trading. A provider composes them; nothing in the host calls
them.
| Type |
Members |
HostedAccounts |
sealed class: the accounts one venue session hosts, on that session's connection and in its mode. It is the check in front of every placement, cancel, modify and flatten. HostedAccounts(string providerKey, Func<IReadOnlyList<string>> venueAccountIds, AccountMode mode, string venueName): the venue's account ids are read each time they are asked for. bool Hosts(AccountId account) is true when the account carries this provider's key, is in the session's mode and its ProviderAccountId is one the venue lists. void Require(AccountId account) is for placement paths: it throws InvalidOperationException when the account is not hosted. Cancel, modify and flatten ask Hosts and answer false instead, because a trader must always be able to unwind. string Known: the hosted venue account ids, for a log line or a refusal that names them. Two connections of one venue can list the same account number, and a practice and a live login can carry the same account id: the key and the mode are both part of the answer. |
PriorSessionOrders |
static: what already rests at a venue when a session starts, in two populations. Orders a previous session of the platform placed (their client reference parses to a platform order id this session does not hold) are said loudly and named, because the trader believes they are still managed and they are not. Everything else the trader placed at the broker gets one line with a count. (IReadOnlyList<TReport> PriorSession, IReadOnlyList<TReport> Foreign) Classify<TReport>(IReadOnlyList<TReport> open, Func<TReport, bool> isWorking, Func<TReport, string> venueOrderId, Func<TReport, string?> clientOrderId, Func<string, bool> placedThisSession, Func<OrderId, bool> heldThisSession) sorts the venue's working orders this session does not manage and returns them as a ValueTuple with the named elements (PriorSession, Foreign); call it under the lock the provider's book is read under. void Say<TReport>(string venue, (IReadOnlyList<TReport> PriorSession, IReadOnlyList<TReport> Foreign) found, Func<TReport, string> describe, Action<string>? log) says each population that has orders, naming the first NamedAtMost (5) of a previous session's orders. Task WarnAsync<TReport>(string venue, Func<CancellationToken, Task<IReadOnlyList<TReport>>> readOpen, Func<IReadOnlyList<TReport>, (IReadOnlyList<TReport> PriorSession, IReadOnlyList<TReport> Foreign)> classify, Func<TReport, string> describe, Action<string>? log, CancellationToken ct) reads, classifies and says in one call. The warning is information, not a gate: a read that fails is said and the start goes on. |
WorkingOrderSet |
sealed class: the working orders of a book as one published list, kept in step statement by statement instead of rebuilt. IReadOnlyList<Order> Current is one volatile read of an immutable array, from any thread, with no lock and no allocation. void Apply(Order order) lists an order on its first non-terminal statement, replaces it in place on a later one and unlists it on a terminal one; it does not judge the statement, so apply only statements the book accepted. void RemoveWhere<TState>(TState state, Func<Order, TState, bool> leaves) unlists, in one pass, the orders of an account the venue withdrew or reset. void Clear() unlists every order. The three writers are not synchronised: call them under the lock that writes the book. BrokerFactsTable.WorkingOrders is published this way; no other SDK member returns or takes the type. |
Polled venues: SabrTrader.Pipeline.Venues.Trading.Polling
For a trading port that learns its positions by polling the venue. The provider's gateway owns and calls
these types.
| Type |
Members |
VenuePollCoverage |
VenuePollCoverage(string venueName, Action<string>? log); bool ReadsEvery(IReadOnlyList<string> announced, IReadOnlyList<string> read): true when read holds every one of announced and is not empty. Only such a cycle may end a connect's replay: a cycle that read no account, or not every one, proves nothing about the accounts it did not read. A poll that keeps missing the same accounts says so once in log, not every cycle. Not thread-safe: call it under the lock that guards the provider's book. |
VenuePollHealth |
VenuePollHealth(string venueName, Action publish, Action<string>? log = null): a polled venue's link health as the session's connection row receives it. long Number() hands out the next statement number; the gateway takes it under the lock its stop and start take, in the same step as its check that the poll is still the running one. void Report(bool healthy, long number) applies the statement unless a later one was already applied, then calls publish, which reads bool Healthy for the state to publish. void Fence(long number): a poll stopped, so every report numbered before number is stale. A report a poll took before it was stopped is dropped however late it arrives. One instance lives as long as the session, across reconnects. Thread-safe. |
PositionPollCycle |
record (long Sequence, IReadOnlyList<string> AccountsRead, long Loop): one position-poll cycle that ran to its end. Sequence is the cycle's number in start order, counted before its first read. AccountsRead holds the venue account ids whose positions the cycle read and stated; an account whose read failed is not among them. Loop names the poll loop that ran the cycle, so a cycle of a stopped or replaced loop can be told from the running one's. A gateway uses it as its own record of a cycle; no other SDK member returns or takes it. |
Day clock & position helpers
| Type |
Members |
TradingDayClock |
TradingDayClock(TimeOnly settlement, TimeZoneInfo exchangeTimeZone); TimeOnly SettlementTime, TimeZoneInfo ExchangeTimeZone; static CmeDefault; DateOnly TradingDay(DateTime utcInstant); DateTime NextBoundaryUtc(DateTime utcInstant). |
IPositionPnlConverter |
decimal? ToAccountCurrency(Position position): convert a position's PnL into the account currency when venues report a foreign PnlCurrency. |
IPositionPropagationDelay |
TimeSpan PositionPropagationDelay { get; }: optional provider facet, the worst-case delay between a fill and the venue's position row reflecting it (TimeSpan.Zero for a pushed feed). Report the worst case, not the average. The broker facts never confirm a position within max(PositionPropagationDelay, BrokerFactsTable.MinimumSettle) of its last execution. |
IPositionRefresh |
Task<Position> RefreshPositionAsync(AccountId account, string instrument, ct): optional provider facet, probed with AsCapability<IPositionRefresh>(). The venue's position of the instrument on the account, fetched now: a flat Position when the venue holds none. Throws when the venue could not be asked (a failed or cancelled request): an unanswered pull is never a flat position. |
OpenTradeAssets |
static: bool IsOpenTrade(string? instrumentKey, TradingProviderCapabilities venue), bool BookIsOpenTrades(TradingProviderCapabilities venue). Futures, forex and crypto perps are open trades; stocks, ETFs, funds, options and spot coins are holdings. Flatten All and the trade copier act only on open trades, so a brokerage position list is never traded as if it were a futures book. IsOpenTrade decides by the row's canonical key when it has one; an unkeyed row is an open trade only when BookIsOpenTrades is true for its venue, which is the venue's own declaration: the TradingProviderCapabilities.BookIsOpenTrades flag. Every other venue is a holding book until a canonical key says otherwise. The platform stamps Position.InstrumentKey at book admission, so a position whose wire symbol resolves to a canonical key is classified by its asset class. |
DuplicateSubmission / RealizedPnLUnavailable / UnrealizedPnLPartial
|
Additional RiskRejection subtypes (ns SabrTrader.Pipeline.Venues.Trading.Risk). DuplicateSubmission: the service refused an order identical to one routed moments earlier. RealizedPnLUnavailable: a PnL cap is armed but the venue does not report realized PnL. UnrealizedPnLPartial (int ExcludedPositions): MaxTotalLoss is armed but the unrealized figure is incomplete. See RiskLimits. |
Helpers a trading provider implementation uses to fold venue reports and keep its book honest. They live in
SabrTrader.Pipeline.Venues.Trading and are documented with the broker chapter in the
Brokerage reference: OrderUpdateFold (with
FoldResult) and VenueOrderReport, OrderRevisionTracker,
WorkingOrderProbe<TOrder, TUpdate>, AmbiguousPlacementResolver,
OrderQuantitySnapper, OrderSizingRules, QuantityStepMath,
StreamGenerationGuard<TKey> and PositionSnapshotReconciler.