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 matchesactiveAccountId. - 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
symbolInfosentry is scoped by its exactsymboland, when rules vary by account, itsaccountId. - Required
accounts,orders,positions, andexecutionscollections are present even when empty. - Capabilities describe available UI choices; they never replace backend authentication, authorization, risk checks, or final order validation.
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.
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.
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.
| Layer | Question it answers | Authority |
|---|---|---|
trading.getOperationSupport() | Can this controller call the operation at all? | Adapter method shape, controller fallbacks, host risk wiring, and deployment entitlements |
TradingFeatureSet | Should this runtime/account session offer the feature now? | getFeatures(), state.features, or a features event |
TradingAccount.capabilities | What choices does this account structurally support? | The selected broker account |
TradingSymbolInfo | What 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
| Surface | Structural operations | Additional broker facts |
|---|---|---|
| Standard order ticket | Required placeOrder; optional previewOrder and modification methods | Account order choices, route rules and durations, current tradability |
| Option order ticket | placeOrder, previewOrder; optionally resolveOptionContract | Exact option contract, account route, option quotes, and route rules |
| Prediction-market ticket | placeOrder, previewOrder | Exact outcome symbol, live outcome quote, tradability, and route rules |
| Chart trading | placeOrder plus only the modify, cancel, and position operations exposed by the UI | Broker orders and positions, exact symbol rules, current market prices |
| Account Manager | getAccountManagerInfo metadata for standard and custom pages; custom loaders and actions only where declared | Runtime features plus state for orders, positions, executions, balances, and history |
| Position actions | The exact close, modify, reverse, flatten, or individual-position methods offered | supportsPositions, netting model, actionable rows, and protection limits |
| Trading terminal | The union required by its mounted child surfaces | One 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
SdkSymbolInfoand 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.