Skip to main content

Accounts, capabilities, and symbol rules

Trading decisions apply to an exact account and SdkSymbolInfo, not to the currently visible ticker text. Your backend remains the authority for which accounts the session may access and what each account/symbol route may trade.

Keep state internally consistent

Every getState() snapshot must satisfy these invariants:

  • activeAccountId, when present, identifies an account in the same snapshot.
  • At most one account has isActive: true, and it matches activeAccountId.
  • Every order, position, and execution retains its backend account id and exact SDK symbol identity. Its published id is globally unique within the controller, even when the provider's native id is only unique inside one account. Net and individual positions also share one id namespace.
  • Each symbolInfos entry is scoped by its exact symbol and, when rules vary by account, its accountId.
  • Required accounts, orders, positions, and executions collections are present even when empty.
  • Capabilities describe available UI choices; they never replace backend authentication, authorization, risk checks, or final order validation.
account-state.ts
import type {
SdkSymbolInfo,
TradingState,
} from '@tradescript/pro/sdk';

export const aapl = {
ticker: 'AAPL',
exchange: 'NASDAQ',
type: 'stock',
} satisfies SdkSymbolInfo;

export const state = {
connectionStatus: 'connected',
activeAccountId: 'acct-margin',
accounts: [{
id: 'acct-margin',
name: 'Margin account',
currency: 'USD',
isActive: true,
capabilities: {
supportsTrading: true,
supportsMargin: true,
supportsShortSelling: true,
supportedOrderTypes: ['market', 'limit', 'stop-limit'],
supportedDurations: [
{ type: 'day', label: 'Day' },
{ type: 'gtc', label: 'GTC' },
],
},
}],
symbolInfos: [{
symbol: aapl,
accountId: 'acct-margin',
minQuantity: 1,
quantityStep: 1,
priceStep: 0.01,
supportedOrderRules: [
{ type: 'market', brokerOrderTypeId: 'MKT', label: 'Market' },
{
type: 'limit',
brokerOrderTypeId: 'LMT',
label: 'Limit',
requiresLimitPrice: true,
},
{
type: 'stop-limit',
brokerOrderTypeId: 'STP_LMT',
label: 'Stop limit',
requiresLimitPrice: true,
requiresStopPrice: true,
},
],
supportedDurations: [
{ type: 'day', label: 'Day' },
{ type: 'gtc', label: 'GTC' },
],
}],
orders: [],
positions: [],
executions: [],
} satisfies TradingState;

Use provider-native account, order-type, and route ids alongside normalized SDK semantics. Never reconstruct them from account names, labels, or ticker text.

List and switch accounts

trading.listAccounts() calls adapter listAccounts() when implemented. When it is absent, the controller falls back to a fresh getState() and returns that snapshot's accounts. Implement listAccounts only when it is a cheaper or more appropriate backend read; it does not refresh orders, positions, executions, or symbol rules.

trading.setActiveAccount(accountId) calls the adapter method and updates the controller's account selection. It does not automatically reload dependent broker state. If the backend snapshot is scoped to the active account, publish a fresh state event containing that account's orders, positions, executions, and symbol rules as part of the switch. If snapshots include all authorized accounts, retain every row's accountId and publish any route facts that changed.

account-routing.ts
import type {
TradingAccount,
TradingBrokerAdapter,
TradingEvent,
TradingOrderContext,
TradingState,
TradingSymbolInfo,
TradingTradableResult,
} from '@tradescript/pro/sdk';

declare function loadAuthorizedAccounts(): Promise<TradingAccount[]>;
declare function selectBackendAccount(accountId: string): Promise<void>;
declare function loadCompleteTradingState(): Promise<TradingState>;
declare function loadRouteRules(
context: TradingOrderContext,
): Promise<Omit<TradingSymbolInfo, 'symbol' | 'accountId'>>;
declare function loadTradability(
context: TradingOrderContext,
): Promise<TradingTradableResult>;

const listeners = new Set<(event: TradingEvent) => void>();

function publish(event: TradingEvent): void {
for (const listener of listeners) listener(event);
}

export const accountAndSymbolMethods = {
async listAccounts() {
return loadAuthorizedAccounts();
},

async setActiveAccount(accountId) {
// The backend authorizes the exact account before changing session scope.
await selectBackendAccount(accountId);

// Account selection alone does not refresh dependent controller slices.
publish({ type: 'state', state: await loadCompleteTradingState() });
},

async isTradable(context) {
return loadTradability(context);
},

async getTradingSymbolInfo(context) {
const rules = await loadRouteRules(context);
return {
...rules,
symbol: context.symbol,
accountId: context.accountId,
};
},

subscribe(callback) {
listeners.add(callback);
return () => {
listeners.delete(callback);
};
},
} satisfies Pick<
TradingBrokerAdapter,
| 'listAccounts'
| 'setActiveAccount'
| 'isTradable'
| 'getTradingSymbolInfo'
| 'subscribe'
>;

Serialize overlapping account switches or discard stale responses with a host-owned request generation. A late response for the previous account must not replace the current account's state or rules.

Resolve one exact trading route

Call isTradable(context) for the backend's current verdict on an exact account/symbol route. A non-tradable result can include a reason and typed solutions such as changing account or symbol. The UI may present those solutions, but it must not infer a route or silently change the requested trade.

getTradingSymbolInfo(context) returns the route's quantity and price steps, notional limits, order rules, durations, short-selling support, bracket limits, and ticket configuration. The controller can fall back to isTradable's symbolRules when getTradingSymbolInfo is absent, but implementing the latter keeps the complete route contract explicit.

Publish the broker's exact quantity step

Set quantityStep to the smallest quantity increment accepted by the broker for the exact symbol/account route. Built-in order-ticket quantity controls use that value directly, including fractional steps. They default to 1 only when the route does not publish a step. Do not round the SDK value to a whole unit or infer it from the asset class: two brokers can support different increments for the same symbol.

fractional-quantity-rules.ts
import type { TradingOrderContext, TradingSymbolInfo } from '@tradescript/pro/sdk';

export async function getTradingSymbolInfo(
context: TradingOrderContext,
): Promise<TradingSymbolInfo> {
return {
symbol: context.symbol,
accountId: context.accountId,
minQuantity: 0.001,
quantityStep: 0.001,
priceStep: 0.01,
};
}

With this contract, each quantity or + action moves by 0.001. Publish a different positive step for routes that support another precision. minQuantity and maxQuantity remain independent route bounds; the backend must still revalidate the submitted quantity against all three values.

Revalidate these rules on the backend when an order is submitted. Cached UI rules improve collection and feedback; they are not execution authority.

Treat capabilities as independent gates

Offer an action only when every relevant gate allows it. A more specific layer may narrow an action, but no runtime or display flag can create a missing controller operation.

LayerQuestion it answersAuthority
trading.getOperationSupport()Can this controller call the operation at all?Adapter method shape, controller fallbacks, host risk wiring, and deployment entitlements
TradingFeatureSetShould this runtime/account session offer the feature now?getFeatures(), state.features, or a features event
TradingAccount.capabilitiesWhat choices does this account structurally support?The selected broker account
TradingSymbolInfoWhat rules apply to this exact account/symbol route?Broker route metadata
isTradable(context)Is the exact route currently tradable, and what remediation is available?Current backend verdict

Structural operation support is resolved when the controller is constructed and remains stable for that controller's lifetime. Each production read is filtered by the active entitlement session. A later authorization-policy change destroys the controller, so the owner must create a replacement. Add optional adapter methods before controller construction; use feature and symbol-info events plus fresh tradability reads for facts that legitimately change during a session.

Map surfaces to the same authority

SurfaceStructural operationsAdditional broker facts
Standard order ticketRequired placeOrder; optional previewOrder and modification methodsAccount order choices, route rules and durations, current tradability
Option order ticketplaceOrder, previewOrder; optionally resolveOptionContractExact option contract, account route, option quotes, and route rules
Prediction-market ticketplaceOrder, previewOrderExact outcome symbol, live outcome quote, tradability, and route rules
Chart tradingplaceOrder plus only the modify, cancel, and position operations exposed by the UIBroker orders and positions, exact symbol rules, current market prices
Account ManagergetAccountManagerInfo metadata for standard and custom pages; custom loaders and actions only where declaredRuntime features plus state for orders, positions, executions, balances, and history
Position actionsThe exact close, modify, reverse, flatten, or individual-position methods offeredsupportsPositions, netting model, actionable rows, and protection limits
Trading terminalThe union required by its mounted child surfacesOne shared controller, accounts, exact rules, broker state, and market data

Live order-entry prices, option quotes, and prediction-outcome quotes come from the market-data authority. Broker-owned position valuation fields such as marketPrice and P&L can still appear in trading state; do not copy live quote facts into account capabilities or infer a quote feed from broker state.

Verify the contract

  • Every active account id and row account id belongs to the authenticated scope.
  • Switching accounts cannot leave the prior account's scoped rows or symbol rules visible.
  • A delayed response for an old account cannot overwrite the current route.
  • getOperationSupport() matches the adapter contract at controller creation.
  • Runtime features and account/symbol rules only narrow structural support.
  • The same exact SdkSymbolInfo and account id reach tradability, preview, placement, and later broker state.
  • Server-side validation rejects stale or unauthorized account/symbol rules even when the UI previously displayed them as available.

Next steps

  • Orders — preserve account, symbol, and operation identity through every mutation.
  • State and events — publish account switches and route changes without stale overwrite.
  • Broker Adapter — add lifecycle, live updates, and optional operations.
  • Order Ticket — render the account and route choices declared here.