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.

Preset wrapper. Brokers live at the engine layer (the IBroker implementation + registry entry described below). The user-facing layer is a preset in src/domain/trading/brokers/preset-catalog.ts that wraps the broker with a Zod schema, an optional mode dropdown, password masking, and a toEngineConfig() translator. Once your broker is registered, add one preset entry so users can pick it from the new-account wizard. The CCXT engine demonstrates the pattern best — one broker class (CcxtBroker), many presets (okx, bybit, hyperliquid, bitget, ccxt-custom).

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>

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

  // Queries
  getAccount(): Promise<AccountInfo>
  getPositions(): Promise<Position[]>
  getOrders(orderIds: string[]): Promise<OpenOrder[]>
  getOrder(orderId: string): Promise<OpenOrder | null>
  getQuote(contract: Contract): Promise<Quote>
  getMarketClock(): Promise<MarketClock>

  // Capabilities
  getCapabilities(): AccountCapabilities

  // Identity
  getNativeKey(contract: Contract): string
  resolveNativeKey(nativeKey: string): Contract
}

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 the engine-level config — populated by the
  // preset's toEngineConfig() from the user-facing presetConfig.
  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