GitHubBlog

Search Documentation

Search for a page in the docs

Custom Brokers

Adding a new broker to OpenAlice requires three things: implement the IBroker interface, declare config schema and UI field descriptors, and register in the broker registry. Zero changes to the framework needed.

IBroker Interface

Every broker implements this interface:

interface IBroker {
  readonly id: string       // Unique account ID
  readonly label: string    // Display name

  // Lifecycle
  init(): Promise<void>
  close(): Promise<void>

  // Contract search
  searchContracts(pattern: string): Promise<ContractDescription[]>
  getContractDetails(query: Contract): Promise<ContractDetails | null>

  // Account data
  getAccount(): Promise<AccountInfo>
  getPositions(): Promise<Position[]>
  getOrders(pendingIds: string[]): Promise<OpenOrder[]>
  getOrder(orderId: string): Promise<OpenOrder | null>

  // Trading
  placeOrder(contract: Contract, order: Order, tpsl?: TpSlParams): Promise<PlaceOrderResult>
  modifyOrder(orderId: string, changes: Partial<Order>): Promise<PlaceOrderResult>
  closePosition(contract: Contract, qty?: Decimal): Promise<PlaceOrderResult>
  cancelOrder(orderId: string, orderCancel?: OrderCancel): Promise<PlaceOrderResult>

  // Market data
  getQuote(contract: Contract): Promise<Quote>
  getMarketClock(): Promise<MarketClock>

  // Identity
  getNativeKey(contract: Contract): string
  resolveNativeKey(nativeKey: string): Contract
  getCapabilities(): AccountCapabilities
  getHealth(): Promise<BrokerHealthInfo>
}

All types (Contract, Order, Execution, OrderState) come from @traderalice/ibkr — this is the single source of truth for the type system.

Config Schema & UI Fields

Each broker declares its config shape and how the Web UI should render the config form:

class MyBroker implements IBroker {
  // Zod schema for validating brokerConfig from accounts.json
  static configSchema = z.object({
    apiKey: z.string(),
    apiSecret: z.string(),
    sandbox: z.boolean().default(false),
  })

  // UI field descriptors for dynamic form rendering
  static configFields: BrokerConfigField[] = [
    { name: 'apiKey', type: 'password', label: 'API Key', required: true, sensitive: true },
    { name: 'apiSecret', type: 'password', label: 'API Secret', required: true, sensitive: true },
    { name: 'sandbox', type: 'boolean', label: 'Sandbox Mode', default: false },
  ]

  // Factory: construct from AccountConfig
  static fromConfig(config: AccountConfig): IBroker {
    const parsed = MyBroker.configSchema.parse(config.brokerConfig)
    return new MyBroker(config.id, config.label ?? config.id, parsed)
  }
}

Register in the Broker Registry

Add one entry to src/domain/trading/brokers/registry.ts:

import { MyBroker } from './my-broker/MyBroker.js'

export const BROKER_REGISTRY: Record<string, BrokerRegistryEntry> = {
  // ... existing brokers
  'my-broker': {
    configSchema: MyBroker.configSchema,
    configFields: MyBroker.configFields,
    fromConfig: MyBroker.fromConfig,
    name: 'My Broker',
    description: 'Description for the account management UI.',
    badge: 'MB',
    badgeColor: 'text-blue-400',
    subtitleFields: [{ field: 'sandbox', label: 'Sandbox' }],
    guardCategory: 'securities',  // or 'crypto'
  },
}

The BrokerRegistryEntry includes:

FieldDescription
configSchemaZod schema for validation
configFieldsUI field descriptors for dynamic form rendering
fromConfigFactory function from AccountConfig
nameDisplay name
descriptionShort description
badge / badgeColor2-3 char badge for the UI
subtitleFieldsFields shown in the account card subtitle
guardCategory"crypto" or "securities" — determines available guard types

Financial Precision

Use decimal.js for all quantity calculations. The Order.totalQuantity field is a Decimal, and Position.quantity must also be a Decimal. Never use JavaScript floating-point for financial math.

Error Handling

Throw BrokerError with appropriate codes:

CodePermanent?Description
CONFIGYesInvalid configuration
AUTHYesAuthentication failure
NETWORKNoNetwork/timeout issues
EXCHANGENoExchange-level rejections
MARKET_CLOSEDNoMarket is closed
UNKNOWNNoUnclassified errors

Permanent errors disable the account. Transient errors trigger auto-recovery with exponential backoff.

Reference

Study the existing implementations for patterns:

  • AlpacaBroker — Simple REST API broker, good starting point
  • CcxtBroker — Adapter pattern over the CCXT library
  • IbkrBroker — Complex callback-based SDK bridged to Promise-based interface