OMS streaming

Subscribe to the current trading day's order, trade, portfolio position and cash detail updates over SignalR. You may use any supported SignalR client to stream data.

At a glance

Scopeoms-streams-api
Testhttps://test-oms-streams-api.openmarkets.com.au/
Productionhttps://oms-streams-api.openmarkets.com.au/
Hub path/streams

The streaming hub is a separate service from the Order Management REST API. It has its own base URL and its own scope, so an oms-api token will not authenticate against it.

How it works

  1. Build a hub connection against /streams on the base URL above, supplying an access token provider.
  2. Register handlers for the events you care about.
  3. Start the connection.
  4. Invoke the subscribe method for each channel you want.

Register handlers before you invoke a subscribe method. The hub sends the initial snapshot before the invoke completes.

Establishing a connection

HubConnection connection = new HubConnectionBuilder()
    .WithUrl("https://test-oms-streams-api.openmarkets.com.au/streams", options =>
    {
        options.AccessTokenProvider = GetBearerToken;
    })
    .AddMessagePackProtocol()
    .Build();

await connection.StartAsync();
import * as signalR from '@microsoft/signalr'
import { MessagePackHubProtocol } from '@microsoft/signalr-protocol-msgpack'

const connection = new signalR.HubConnectionBuilder()
  .withUrl('https://test-oms-streams-api.openmarkets.com.au/streams', {
    accessTokenFactory: () => getBearerToken(),
  })
  .withHubProtocol(new MessagePackHubProtocol())
  .build()

await connection.start()

The hub supports the JSON and MessagePack protocols. MessagePack produces smaller messages. With MessagePack, field names are PascalCase (for example OrderNumber) and dates are MessagePack timestamps. With JSON, field names are camelCase as in the schemas below, dates are ISO-8601 strings, and portfolio positions and cash details also carry a key field.

Providing an access token

The hub authenticates with the same kind of bearer token as the REST APIs, so the token provider is just a function that returns a current token carrying the oms-streams-api scope claim. See Authentication. In production your client must also be approved for live access.

private async Task<string> GetBearerToken()
{
    // Return a cached token, refreshing it when it is close to expiry.
}
async function getBearerToken() {
  // Return a cached token, refreshing it when it is close to expiry.
}

The provider is called on connect and on every reconnect, so returning a cached token that you refresh near expiry keeps long-lived connections authenticated without extra token requests.

Channels

None of the OMS subscribe methods take parameters, and none return a value. Each subscribes you to the records for your business's advisor codes. If your business has no advisor codes, you receive the records for your business's trading accounts. There are no unsubscribe methods: disconnect to stop receiving updates.

EventSubscribe methodPayload
OrdersUpdatedSubscribeToOrderUpdates()Order[]
TradesUpdatedSubscribeToTradeUpdates()Trade[]
PortfolioPositionsUpdatedSubscribeToPortfolioPositionUpdates()PortfolioPosition[]
PortfolioCashDetailsUpdatedSubscribeToPortfolioCashDetailUpdates()PortfolioCashDetail[]

When you subscribe, the hub first sends everything it holds for your business since the daily reset at 07:10 Sydney time, as one message. After that, each change arrives as a one-element array. Use the REST API for anything earlier.

A subscribe method throws a HubException in these cases:

MessageCause
Internal server error.The token does not identify a business.
Failed to subscribe to ... updates. Client does not have any permissions set.Your business has no advisor codes and no trading accounts.
Client already has an existing subscription.Your client already has two connections subscribed to this channel.

Subscribing

Register handlers, start the connection, then subscribe.

private void HandleOrdersUpdated()
{
    this.connection.On<Order[]>("OrdersUpdated", orders =>
    {
        // Do something with the data
    });
}

private async Task SubscribeToOrderUpdates()
{
    await this.connection.InvokeAsync("SubscribeToOrderUpdates");
}

// ...

this.HandleOrdersUpdated();
await connection.StartAsync();
await this.SubscribeToOrderUpdates();
connection.on('OrdersUpdated', (orders) => {
  // Do something with the data
})

await connection.start()

await connection.invoke('SubscribeToOrderUpdates')

The other three channels follow the same shape: register the handler, then invoke the matching subscribe method.

Automatic reconnect

Connections dropped by network issues can be re-established automatically.

HubConnection connection = new HubConnectionBuilder()
    .WithUrl("https://test-oms-streams-api.openmarkets.com.au/streams", options =>
    {
        options.AccessTokenProvider = GetBearerToken;
    })
    .WithAutomaticReconnect() // this will enable automatic reconnect
    .AddMessagePackProtocol()
    .Build();
const connection = new signalR.HubConnectionBuilder()
  .withUrl('https://test-oms-streams-api.openmarkets.com.au/streams', {
    accessTokenFactory: () => getBearerToken(),
  })
  .withAutomaticReconnect() // this will enable automatic reconnect
  .withHubProtocol(new MessagePackHubProtocol())
  .build()

The default retry policy makes 4 attempts, waiting 0, 2, 10 and 30 seconds respectively. You can supply your own policy. See Microsoft's client features documentation.

Handling reconnects

A reconnected client does not retain its subscriptions. Resubscribe to every channel you use in a Reconnected handler, or the connection will appear healthy while delivering nothing. Each resubscribe sends the full snapshot again, so merge records by orderNumber, tradeNumber, portfolio code with security code and exchange, or portfolioCashCode.

connection.Reconnected += async (connectionId) =>
{
    Console.WriteLine("Reconnected.");

    // Resubscribe to the channels
    await SubscribeToOrderUpdates();
    await SubscribeToTradeUpdates();
};
connection.onreconnected(async (connectionId) => {
  console.log('Reconnected.')

  // Resubscribe to the channels
  await connection.invoke('SubscribeToOrderUpdates')
  await connection.invoke('SubscribeToTradeUpdates')
})

A Reconnecting handler is optional, and is useful if you want to surface connection state in your own application while the client retries.

Order schema

{
	"rootParentOrderNumber": { "type": "number" },
	"orderNumber": { "type": "number" },
	"parentOrderNumber": { "type": "number" },
	"accountCode": { "type": "string" },
	"securityCode": { "type": "string" },
	"exchange": { "type": "string" },
	"destination": { "type": "string" },
	"subDestination": { "type": "string" },
	"pricingInstructions": { "type": "string" },
	"orderState": { "type": "string" },
	"lastAction": { "type": "string" },
	"actionStatus": { "type": "string" },
	"orderVolume": { "type": "number" },
	"orderPrice": { "type": "number" },
	"remainingVolume": { "type": "number" },
	"doneVolumeTotal": { "type": "number" },
	"doneValueTotal": { "type": "number" },
	"uncommittedVolume": { "type": "number" },
	"averagePrice": { "type": "number" },
	"lifetime": { "type": "string" },
	"currency": { "type": "string" },
	"expiryDateTime": { "type": "datetime" },
	"doneVolumeToday": { "type": "number" },
	"doneValueToday": { "type": "number" },
	"stateDescription": { "type": "string" },
	"createDateTime": { "type": "datetime" },
	"updateDateTime": { "type": "datetime" },
	"destinationVolume": { "type": "number" },
	"destinationPrice": { "type": "number" },
	"destinationStatus": { "type": "string" },
	"destinationOrderNumber": { "type": "string" },
	"principal": { "type": "bool" },
	"orderMatchId": { "type": "string" },
	"marketDataOrderNumber": { "type": "number" },
	"side": { "type": "string" },
	"effectiveDoneVolume": { "type": "number" },
	"settlementDoneValueTotal": { "type": "number" },
	"settlementDoneValueToday": { "type": "number" },
	"settlementAveragePrice": { "type": "number" },
	"clientSequenceNumber": { "type": "number" },
	"marketDetail": { "type": "string" },
	"postTradeStatusNumber": { "type": "number" },
	"postTradeStatus": { "type": "string" },
	"estimatedPrice": { "type": "number" },
	"orderValue": { "type": "number" },
	"advisorCode": { "type": "string" },
	"estimatedVolume": { "type": "number" },
	"estimatedValue": { "type": "number" },
	"notes": { "type": "string" },
	"dataFields": { "type": "object" },
	"orderGiver": { "type": "string" },
	"orderTaker": { "type": "string" }
}

Every field except orderNumber can be null.

FieldValues
sideBuy, Sell, Short, Undetermined
lifetimeDate, EndOfDay, GoodTillCancel, GoodTillTime, FillAndKill, FillOrKill
pricingInstructionsLimit, MarketToLimit, DarkLimit, Best, Market, AnyPriceBlock, AuctionImbalance, MarketOnClose, CpMarket, CpLimit
postTradeStatusNone, Open, PartialFill, Filled, NoticeOfExecutionSent, Matched, Ticketed, Confirmed

dataFields is a map of string values. Only the keys that apply to the order are present:

KeyValues
fixedContingentOrder.triggerSecuritySecurity in the form securityCode.exchange
fixedContingentOrder.triggerPriceTypeTrigger price type
fixedContingentOrder.triggerPriceTrigger price
fixedContingentOrder.triggerConditionGreater, Less, GreaterOrEqual, LessOrEqual
fixedContingentOrder.contingentOrderStatusActive, Triggered, Deleted, PreActive
brokerageOverride.minimumBrokerage override minimum
brokerageOverride.percentageBrokerage override percentage
originOfOrderOrigin of order

Trade schema

{
	"advisorCode": { "type": "string" },
	"tradeNumber": { "type": "number" },
	"orderNumber": { "type": "number" },
	"accountCode": { "type": "string" },
	"securityCode": { "type": "string" },
	"exchange": { "type": "string" },
	"destination": { "type": "string" },
	"subDestination": { "type": "string" },
	"buyOrSell": { "type": "string" },
	"tradeVolume": { "type": "number" },
	"tradePrice": { "type": "number" },
	"tradeValue": { "type": "number" },
	"tradeFxRate": { "type": "number" },
	"tradeDateTime": { "type": "datetime" },
	"principal": { "type": "boolean" },
	"opposingBrokerNumber": { "type": "number" },
	"primaryClientOrderId": { "type": "string" },
	"secondaryClientOrderId": { "type": "string" },
	"tradeMarkers": { "type": "string" },
	"destinationUserId": { "type": "string" },
	"destinationOrderNumber": { "type": "string" },
	"destinationTradeNumber": { "type": "string" },
	"cancelledByTradeNumber": { "type": "number" },
	"marketDataOrderNumber": { "type": "number" },
	"marketDataTradeNumber": { "type": "number" },
	"fxRateBidPriceOnOrder": { "type": "number" },
	"fxRateAskPriceOnOrder": { "type": "number" },
	"fxRateBidPriceOnTrade": { "type": "number" },
	"fxRateAskPriceOnTrade": { "type": "number" },
	"sourcePrice": { "type": "number" },
	"sourceCurrency": { "type": "string" },
	"sideCode": { "type": "string" },
	"orderDetails": { "type": "string" },
	"settlementValue": { "type": "number" },
	"settlementFxRate": { "type": "number" },
	"settlementPrice": { "type": "number" },
	"organization": { "type": "string" },
	"bookingDestination": { "type": "string" },
	"tradeMarketDetail": { "type": "string" },
	"postTradeStatusNumber": { "type": "number" },
	"tradeSequenceNumber": { "type": "number" },
	"tradeDateTimeGmt": { "type": "datetime" },
	"exchangeTradeDateTime": { "type": "datetime" },
	"localMarketTradeDate": { "type": "datetime" }
}

Every field except tradeNumber and orderNumber can be null.

Portfolio position schema

{
	"advisorCode": { "type": "string" },
	"accountCode": { "type": "string" },
	"sharingCashAccount": { "type": "boolean" },
	"portfolioCode": { "type": "string" },
	"securityCode": { "type": "string" },
	"exchange": { "type": "string" },
	"underlyingSecurityCode": { "type": "string" },
	"underlyingExchange": { "type": "string" },
	"versionStamp": { "type": "number" },
	"updateReasonMask": { "type": "number" },
	"createDateTime": { "type": "datetime" },
	"updateDateTime": { "type": "datetime" },
	"volumeStartOfDay": { "type": "number" },
	"averagePriceStartOfDay": { "type": "number" },
	"averagePrice": { "type": "number" },
	"buyVolume": { "type": "number" },
	"buyValue": { "type": "number" },
	"buyCharges": { "type": "number" },
	"sellVolume": { "type": "number" },
	"sellValue": { "type": "number" },
	"sellCharges": { "type": "number" },
	"cfdLodgedVolume": { "type": "number" },
	"optionLodgedVolume": { "type": "number" },
	"systemLockedVolume": { "type": "number" },
	"userLockedVolume": { "type": "number" },
	"pledgedTakeOverVolume": { "type": "number" },
	"inMarketBuyVolume": { "type": "number" },
	"inMarketBuyValue": { "type": "number" },
	"inMarketSellVolume": { "type": "number" },
	"inMarketSellValue": { "type": "number" },
	"availableVolume": { "type": "number" },
	"costValue": { "type": "number" },
	"marketValue": { "type": "number" },
	"totalProfit": { "type": "number" },
	"todayProfit": { "type": "number" },
	"closedProfit": { "type": "number" },
	"exposure": { "type": "number" },
	"sfcVariationMargin": { "type": "number" },
	"averageBuyPrice": { "type": "number" },
	"averageSellPrice": { "type": "number" },
	"actualVolume": { "type": "number" },
	"shortSellVolume": { "type": "number" },
	"historicalPrice": { "type": "number" },
	"historicalProfit": { "type": "number" },
	"hedgeExp": { "type": "number" },
	"actualValue": { "type": "number" },
	"unsponsoredBuyVolume": { "type": "number" },
	"unsponsoredBuyValue": { "type": "number" },
	"unsponsoredSellVolume": { "type": "number" },
	"unsponsoredSellValue": { "type": "number" },
	"unsponsoredInMarketBuyVolume": { "type": "number" },
	"unsponsoredInMarketBuyValue": { "type": "number" },
	"unsponsoredInMarketSellVolume": { "type": "number" },
	"unsponsoredInMarketSellValue": { "type": "number" },
	"unsponsoredStartOfDayVolume": { "type": "number" },
	"historicalProfitStartOfDay": { "type": "number" },
	"totalHistoricalProfit": { "type": "number" },
	"totalDiffMktValProfitDay": { "type": "number" },
	"realizedLossAccumulatedTodayValue": { "type": "number" },
	"securityType": { "type": "number" }
}

Every field can be null.

Portfolio cash detail schema

{
	"portfolioCode": { "type": "string" },
	"accountCode": { "type": "string" },
	"advisorCode": { "type": "string" },
	"portfolioCashCode": { "type": "string" },
	"portfolioCashName": { "type": "string" },
	"versionStamp": { "type": "number" },
	"createDateTime": { "type": "datetime" },
	"updateDateTime": { "type": "datetime" },
	"currencyCode": { "type": "string" },
	"cashBalance": { "type": "number" },
	"unsettledBuyValue": { "type": "number" },
	"unsettledBuyCharges": { "type": "number" },
	"unsettledSellValue": { "type": "number" },
	"unsettledSellCharges": { "type": "number" },
	"yesterdayEquitySellValue": { "type": "number" },
	"yesterdayEquitySellCharges": { "type": "number" },
	"inMarketBuyValue": { "type": "number" },
	"inMarketSellValue": { "type": "number" },
	"netCash": { "type": "number" },
	"uploadSource": { "type": "string" },
	"optionUnsettledBuyValue": { "type": "number" },
	"optionUnsettledBuyCharges": { "type": "number" },
	"optionUnsettledSellValue": { "type": "number" },
	"optionUnsettledSellCharges": { "type": "number" },
	"clearingHouseMargin": { "type": "number" },
	"externalValue": { "type": "string" },
	"netUnsettledBuyValueToday": { "type": "number" },
	"netUnsettledSellValueToday": { "type": "number" },
	"netUnsettledValueToday": { "type": "number" },
	"glv": { "type": "number" },
	"freeEquity": { "type": "number" },
	"totalInitialMargin": { "type": "number" },
	"totalCfdRealisedProfit": { "type": "number" },
	"totalCfdUnrealisedProfit": { "type": "number" },
	"totalCfdCollateralValue": { "type": "number" },
	"totalNonCfdMarketValue": { "type": "number" },
	"realizedLossStartOfDayValue": { "type": "number" },
	"marginLenderTotalFinancedValue": { "type": "number" },
	"trustBalance": { "type": "number" },
	"totalCfdRealizedProfitInSettlementCurrency": { "type": "number" },
	"accruedInterest": { "type": "number" },
	"facilityLimit": { "type": "number" },
	"multiSettlementCalculationMethod": { "type": "number" },
	"defaultCashSettlementDays": { "type": "number" },
	"isDefaultCurrency": { "type": "number" }
}

Every field can be null.